diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index eac831e90..e31ac333a 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -79,10 +79,11 @@ jobs: - name: Update Graphs run: ./gradlew graphUpdate + continue-on-error: true - name: Check Graphs id: graphs_verify - run: git add -- '**/README.md' && git diff --cached --quiet --exit-code -- '**/README.md' + run: git add -- "**/README.md" && git diff --cached --quiet --exit-code -- "**/README.md" - name: Prevent updating graphs if this is a fork id: checkfork_graphs 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/README.md b/app/README.md index 3fb136547..cbcdc35be 100644 --- a/app/README.md +++ b/app/README.md @@ -11,6 +11,42 @@ config: nodePlacementStrategy: SIMPLE --- graph TB + subgraph :feature + direction TB + subgraph :feature:settings + direction TB + :feature:settings:impl[impl]:::android-library + end + subgraph :feature:foryou + direction TB + :feature:foryou:api[api]:::android-library + :feature:foryou:impl[impl]:::android-library + end + subgraph :feature:bookmarks + direction TB + :feature:bookmarks:api[api]:::android-library + :feature:bookmarks:impl[impl]:::android-library + end + subgraph :feature:search + direction TB + :feature:search:api[api]:::android-library + :feature:search:impl[impl]:::android-library + end + subgraph :feature:interests + direction TB + :feature:interests:api[api]:::android-library + :feature:interests:impl[impl]:::android-library + end + subgraph :feature:topic + direction TB + :feature:topic:api[api]:::android-library + :feature:topic:impl[impl]:::android-library + end + end + subgraph :sync + direction TB + :sync:work[work]:::android-library + end subgraph :core direction TB :core:analytics[analytics]:::android-library @@ -22,23 +58,11 @@ graph TB :core:designsystem[designsystem]:::android-library :core:domain[domain]:::android-library :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library :core:network[network]:::android-library :core:notifications[notifications]:::android-library :core:ui[ui]:::android-library end - subgraph :feature - direction TB - :feature:bookmarks[bookmarks]:::android-feature - :feature:foryou[foryou]:::android-feature - :feature:interests[interests]:::android-feature - :feature:search[search]:::android-feature - :feature:settings[settings]:::android-feature - :feature:topic[topic]:::android-feature - end - subgraph :sync - direction TB - :sync:work[work]:::android-library - end :benchmarks[benchmarks]:::android-test :app[app]:::android-application @@ -49,12 +73,17 @@ graph TB :app -.-> :core:designsystem :app -.-> :core:model :app -.-> :core:ui - :app -.-> :feature:bookmarks - :app -.-> :feature:foryou - :app -.-> :feature:interests - :app -.-> :feature:search - :app -.-> :feature:settings - :app -.-> :feature:topic + :app -.-> :feature:bookmarks:api + :app -.-> :feature:bookmarks:impl + :app -.-> :feature:foryou:api + :app -.-> :feature:foryou:impl + :app -.-> :feature:interests:api + :app -.-> :feature:interests:impl + :app -.-> :feature:search:api + :app -.-> :feature:search:impl + :app -.-> :feature:settings:impl + :app -.-> :feature:topic:api + :app -.-> :feature:topic:impl :app -.-> :sync:work :benchmarks -.->|testedApks| :app :core:data -.-> :core:analytics @@ -76,28 +105,43 @@ graph TB :core:ui --> :core:analytics :core:ui --> :core:designsystem :core:ui --> :core:model - :feature:bookmarks -.-> :core:data - :feature:bookmarks -.-> :core:designsystem - :feature:bookmarks -.-> :core:ui - :feature:foryou -.-> :core:data - :feature:foryou -.-> :core:designsystem - :feature:foryou -.-> :core:domain - :feature:foryou -.-> :core:notifications - :feature:foryou -.-> :core:ui - :feature:interests -.-> :core:data - :feature:interests -.-> :core:designsystem - :feature:interests -.-> :core:domain - :feature:interests -.-> :core:ui - :feature:search -.-> :core:data - :feature:search -.-> :core:designsystem - :feature:search -.-> :core:domain - :feature:search -.-> :core:ui - :feature:settings -.-> :core:data - :feature:settings -.-> :core:designsystem - :feature:settings -.-> :core:ui - :feature:topic -.-> :core:data - :feature:topic -.-> :core:designsystem - :feature:topic -.-> :core:ui + :feature:bookmarks:api --> :core:navigation + :feature:bookmarks:impl -.-> :core:data + :feature:bookmarks:impl -.-> :core:designsystem + :feature:bookmarks:impl -.-> :core:ui + :feature:bookmarks:impl -.-> :feature:bookmarks:api + :feature:bookmarks:impl -.-> :feature:topic:api + :feature:foryou:api --> :core:navigation + :feature:foryou:impl -.-> :core:designsystem + :feature:foryou:impl -.-> :core:domain + :feature:foryou:impl -.-> :core:notifications + :feature:foryou:impl -.-> :core:ui + :feature:foryou:impl -.-> :feature:foryou:api + :feature:foryou:impl -.-> :feature:topic:api + :feature:interests:api --> :core:navigation + :feature:interests:impl -.-> :core:designsystem + :feature:interests:impl -.-> :core:domain + :feature:interests:impl -.-> :core:ui + :feature:interests:impl -.-> :feature:interests:api + :feature:interests:impl -.-> :feature:topic:api + :feature:search:api -.-> :core:domain + :feature:search:api --> :core:navigation + :feature:search:impl -.-> :core:designsystem + :feature:search:impl -.-> :core:domain + :feature:search:impl -.-> :core:ui + :feature:search:impl -.-> :feature:interests:api + :feature:search:impl -.-> :feature:search:api + :feature:search:impl -.-> :feature:topic:api + :feature:settings:impl -.-> :core:data + :feature:settings:impl -.-> :core:designsystem + :feature:settings:impl -.-> :core:ui + :feature:topic:api -.-> :core:designsystem + :feature:topic:api --> :core:navigation + :feature:topic:api -.-> :core:ui + :feature:topic:impl -.-> :core:data + :feature:topic:impl -.-> :core:designsystem + :feature:topic:impl -.-> :core:ui + :feature:topic:impl -.-> :feature:topic:api :sync:work -.-> :core:analytics :sync:work -.-> :core:data :sync:work -.-> :core:notifications diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 09005606b..2f0253943 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -68,12 +68,17 @@ android { } dependencies { - implementation(projects.feature.interests) - implementation(projects.feature.foryou) - implementation(projects.feature.bookmarks) - implementation(projects.feature.topic) - implementation(projects.feature.search) - implementation(projects.feature.settings) + implementation(projects.feature.interests.api) + implementation(projects.feature.interests.impl) + implementation(projects.feature.foryou.api) + implementation(projects.feature.foryou.impl) + implementation(projects.feature.bookmarks.api) + implementation(projects.feature.bookmarks.impl) + implementation(projects.feature.topic.api) + implementation(projects.feature.topic.impl) + implementation(projects.feature.search.api) + implementation(projects.feature.search.impl) + implementation(projects.feature.settings.impl) implementation(projects.core.common) implementation(projects.core.ui) @@ -85,16 +90,17 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.material3.adaptive.layout) implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.navigation3) implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.compose.runtime.tracing) implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) - implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.lifecycle.runtimeCompose) - implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.lifecycle.viewModel.navigation3) implementation(libs.androidx.profileinstaller) implementation(libs.androidx.tracing.ktx) implementation(libs.androidx.window.core) diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt index f4f1e35c8..7bea0cb3e 100644 --- a/app/dependencies/prodReleaseRuntimeClasspath.txt +++ b/app/dependencies/prodReleaseRuntimeClasspath.txt @@ -1,6 +1,6 @@ -androidx.activity:activity-compose:1.10.1 -androidx.activity:activity-ktx:1.10.1 -androidx.activity:activity:1.10.1 +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,54 +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-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 @@ -70,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 @@ -91,40 +97,45 @@ androidx.graphics:graphics-path:1.0.1 androidx.graphics:graphics-shapes-android:1.0.1 androidx.graphics:graphics-shapes:1.0.1 androidx.hilt:hilt-common:1.2.0 -androidx.hilt:hilt-navigation-compose:1.2.0 -androidx.hilt:hilt-navigation:1.2.0 +androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0-alpha02 +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-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.navigation:navigation-common-ktx:2.8.5 -androidx.navigation:navigation-common:2.8.5 -androidx.navigation:navigation-compose:2.8.5 -androidx.navigation:navigation-runtime-ktx:2.8.5 -androidx.navigation:navigation-runtime:2.8.5 +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 @@ -135,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 @@ -153,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 @@ -212,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 @@ -224,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/prodRelease-badging.txt b/app/prodRelease-badging.txt index 640949857..8bd154572 100644 --- a/app/prodRelease-badging.txt +++ b/app/prodRelease-badging.txt @@ -1,6 +1,6 @@ -package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15' +package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='16' platformBuildVersionCode='36' compileSdkVersion='36' compileSdkVersionCodename='16' minSdkVersion:'23' -targetSdkVersion:'35' +targetSdkVersion:'36' uses-permission: name='android.permission.INTERNET' uses-permission: name='android.permission.ACCESS_NETWORK_STATE' uses-permission: name='android.permission.POST_NOTIFICATIONS' @@ -105,9 +105,9 @@ application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml' application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml' application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml' launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon='' +uses-library-not-required:'android.ext.adservices' uses-library-not-required:'androidx.window.extensions' uses-library-not-required:'androidx.window.sidecar' -uses-library-not-required:'android.ext.adservices' feature-group: label='' uses-feature: name='android.hardware.faketouch' uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps' diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt index 54053a1bb..c0eba5fd3 100644 --- a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.ui import androidx.compose.ui.semantics.SemanticsActions.ScrollBy import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.hasTestTag @@ -39,18 +40,20 @@ import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule +import com.google.samples.apps.nowinandroid.feature.interests.impl.LIST_PANE_TEST_TAG import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import javax.inject.Inject -import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR -import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR -import com.google.samples.apps.nowinandroid.feature.search.R as FeatureSearchR -import com.google.samples.apps.nowinandroid.feature.settings.R as SettingsR +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as BookmarksR +import com.google.samples.apps.nowinandroid.feature.foryou.api.R as FeatureForyouR +import com.google.samples.apps.nowinandroid.feature.search.api.R as FeatureSearchR +import com.google.samples.apps.nowinandroid.feature.settings.impl.R as SettingsR /** * Tests all the navigation flows that are handled by the navigation library. @@ -83,15 +86,15 @@ class NavigationTest { lateinit var newsRepository: NewsRepository // The strings used for matching in these tests - private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_navigate_up) - private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_title) - private val interests by composeTestRule.stringResource(FeatureSearchR.string.feature_search_interests) + private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_api_navigate_up) + private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_api_title) + private val interests by composeTestRule.stringResource(FeatureSearchR.string.feature_search_api_interests) private val sampleTopic = "Headlines" private val appName by composeTestRule.stringResource(R.string.app_name) - private val saved by composeTestRule.stringResource(BookmarksR.string.feature_bookmarks_title) - private val settings by composeTestRule.stringResource(SettingsR.string.feature_settings_top_app_bar_action_icon_description) - private val brand by composeTestRule.stringResource(SettingsR.string.feature_settings_brand_android) - private val ok by composeTestRule.stringResource(SettingsR.string.feature_settings_dismiss_dialog_button_text) + private val saved by composeTestRule.stringResource(BookmarksR.string.feature_bookmarks_api_title) + private val settings by composeTestRule.stringResource(SettingsR.string.feature_settings_impl_top_app_bar_action_icon_description) + private val brand by composeTestRule.stringResource(SettingsR.string.feature_settings_impl_brand_android) + private val ok by composeTestRule.stringResource(SettingsR.string.feature_settings_impl_dismiss_dialog_button_text) @Before fun setup() = hiltRule.inject() @@ -252,6 +255,9 @@ class NavigationTest { } } + // TODO decide if backStack should preserve previous stacks when navigating back to home tab (ForYou) + // https://github.com/android/nowinandroid/issues/1937 + @Ignore @Test fun navigationBar_multipleBackStackInterests() { composeTestRule.apply { @@ -261,12 +267,14 @@ class NavigationTest { val topic = runBlocking { topicsRepository.getTopics().first().sortedBy(Topic::name).last() } - onNodeWithTag("interests:topics").performScrollToNode(hasText(topic.name)) + onNodeWithTag(LIST_PANE_TEST_TAG).performScrollToNode(hasText(topic.name)) onNodeWithText(topic.name).performClick() + // Verify the topic is still shown + onNodeWithTag("topic:${topic.id}").assertIsDisplayed() + // Switch tab onNodeWithText(forYou).performClick() - // Come back to Interests onNodeWithText(interests).performClick() diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt deleted file mode 100644 index e079c98f4..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2022 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.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.compose.NavHost -import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouSection -import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests -import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen -import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic -import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS -import com.google.samples.apps.nowinandroid.ui.NiaAppState -import com.google.samples.apps.nowinandroid.ui.interests2pane.interestsListDetailScreen - -/** - * Top-level navigation graph. Navigation is organized as explained at - * https://d.android.com/jetpack/compose/nav-adaptive - * - * The navigation graph defined in this file defines the different top level routes. Navigation - * within each route is handled using state and Back Handlers. - */ -@Composable -fun NiaNavHost( - appState: NiaAppState, - onShowSnackbar: suspend (String, String?) -> Boolean, - modifier: Modifier = Modifier, -) { - val navController = appState.navController - NavHost( - navController = navController, - startDestination = ForYouBaseRoute, - modifier = modifier, - ) { - forYouSection( - onTopicClick = navController::navigateToTopic, - ) { - topicScreen( - showBackButton = true, - onBackClick = navController::popBackStack, - onTopicClick = navController::navigateToTopic, - ) - } - bookmarksScreen( - onTopicClick = navController::navigateToInterests, - onShowSnackbar = onShowSnackbar, - ) - searchScreen( - onBackClick = navController::popBackStack, - onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) }, - onTopicClick = navController::navigateToInterests, - ) - interestsListDetailScreen() - } -} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt deleted file mode 100644 index 429e626ff..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2022 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.navigation - -import androidx.annotation.StringRes -import androidx.compose.ui.graphics.vector.ImageVector -import com.google.samples.apps.nowinandroid.R -import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons -import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksRoute -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute -import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute -import kotlin.reflect.KClass -import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR -import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR -import com.google.samples.apps.nowinandroid.feature.search.R as searchR - -/** - * Type for the top level destinations in the application. Contains metadata about the destination - * that is used in the top app bar and common navigation UI. - * - * @param selectedIcon The icon to be displayed in the navigation UI when this destination is - * selected. - * @param unselectedIcon The icon to be displayed in the navigation UI when this destination is - * not selected. - * @param iconTextId Text that to be displayed in the navigation UI. - * @param titleTextId Text that is displayed on the top app bar. - * @param route The route to use when navigating to this destination. - * @param baseRoute The highest ancestor of this destination. Defaults to [route], meaning that - * there is a single destination in that section of the app (no nested destinations). - */ -enum class TopLevelDestination( - val selectedIcon: ImageVector, - val unselectedIcon: ImageVector, - @StringRes val iconTextId: Int, - @StringRes val titleTextId: Int, - val route: KClass<*>, - val baseRoute: KClass<*> = route, -) { - FOR_YOU( - selectedIcon = NiaIcons.Upcoming, - unselectedIcon = NiaIcons.UpcomingBorder, - iconTextId = forYouR.string.feature_foryou_title, - titleTextId = R.string.app_name, - route = ForYouRoute::class, - baseRoute = ForYouBaseRoute::class, - ), - BOOKMARKS( - selectedIcon = NiaIcons.Bookmarks, - unselectedIcon = NiaIcons.BookmarksBorder, - iconTextId = bookmarksR.string.feature_bookmarks_title, - titleTextId = bookmarksR.string.feature_bookmarks_title, - route = BookmarksRoute::class, - ), - INTERESTS( - selectedIcon = NiaIcons.Grid3x3, - unselectedIcon = NiaIcons.Grid3x3, - iconTextId = searchR.string.feature_search_interests, - titleTextId = searchR.string.feature_search_interests, - route = InterestsRoute::class, - ), -} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt new file mode 100644 index 000000000..7021e4fa0 --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt @@ -0,0 +1,73 @@ +/* + * 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.navigation + +import androidx.annotation.StringRes +import androidx.compose.ui.graphics.vector.ImageVector +import com.google.samples.apps.nowinandroid.R +import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey +import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as bookmarksR +import com.google.samples.apps.nowinandroid.feature.foryou.api.R as forYouR +import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR + +/** + * Type for the top level navigation items in the application. Contains UI information about the + * current route that is used in the top app bar and common navigation UI. + * + * @param selectedIcon The icon to be displayed in the navigation UI when this destination is + * selected. + * @param unselectedIcon The icon to be displayed in the navigation UI when this destination is + * not selected. + * @param iconTextId Text that to be displayed in the navigation UI. + * @param titleTextId Text that is displayed on the top app bar. + */ +data class TopLevelNavItem( + val selectedIcon: ImageVector, + val unselectedIcon: ImageVector, + @StringRes val iconTextId: Int, + @StringRes val titleTextId: Int, +) + +val FOR_YOU = TopLevelNavItem( + selectedIcon = NiaIcons.Upcoming, + unselectedIcon = NiaIcons.UpcomingBorder, + iconTextId = forYouR.string.feature_foryou_api_title, + titleTextId = R.string.app_name, +) + +val BOOKMARKS = TopLevelNavItem( + selectedIcon = NiaIcons.Bookmarks, + unselectedIcon = NiaIcons.BookmarksBorder, + iconTextId = bookmarksR.string.feature_bookmarks_api_title, + titleTextId = bookmarksR.string.feature_bookmarks_api_title, +) + +val INTERESTS = TopLevelNavItem( + selectedIcon = NiaIcons.Grid3x3, + unselectedIcon = NiaIcons.Grid3x3, + iconTextId = searchR.string.feature_search_api_interests, + titleTextId = searchR.string.feature_search_api_interests, +) + +val TOP_LEVEL_NAV_ITEMS = mapOf( + ForYouNavKey to FOR_YOU, + BookmarksNavKey to BOOKMARKS, + InterestsNavKey(null) to INTERESTS, +) 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 b237684ef..bfaa27fa6 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 @@ -33,15 +33,16 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration.Indefinite -import androidx.compose.material3.SnackbarDuration.Short import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -60,9 +61,9 @@ 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.navigation.NavDestination -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay 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 @@ -71,11 +72,19 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAp import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors -import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog -import com.google.samples.apps.nowinandroid.navigation.NiaNavHost -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination -import kotlin.reflect.KClass -import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.core.navigation.toEntries +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.bookmarksEntry +import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey +import com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation.forYouEntry +import com.google.samples.apps.nowinandroid.feature.interests.impl.navigation.interestsEntry +import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchNavKey +import com.google.samples.apps.nowinandroid.feature.search.impl.navigation.searchEntry +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsDialog +import com.google.samples.apps.nowinandroid.feature.topic.impl.navigation.topicEntry +import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS +import com.google.samples.apps.nowinandroid.feature.settings.impl.R as settingsR @Composable fun NiaApp( @@ -83,8 +92,7 @@ fun NiaApp( modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { - val shouldShowGradientBackground = - appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU + val shouldShowGradientBackground = appState.navigationState.currentTopLevelKey == ForYouNavKey var showSettingsDialog by rememberSaveable { mutableStateOf(false) } NiaBackground(modifier = modifier) { @@ -109,15 +117,17 @@ fun NiaApp( ) } } + CompositionLocalProvider(LocalSnackbarHostState provides snackbarHostState) { + NiaApp( + appState = appState, - NiaApp( - appState = appState, - snackbarHostState = snackbarHostState, - showSettingsDialog = showSettingsDialog, - onSettingsDismissed = { showSettingsDialog = false }, - onTopAppBarActionClick = { showSettingsDialog = true }, - windowAdaptiveInfo = windowAdaptiveInfo, - ) + // TODO: Settings should be a dialog screen + showSettingsDialog = showSettingsDialog, + onSettingsDismissed = { showSettingsDialog = false }, + onTopAppBarActionClick = { showSettingsDialog = true }, + windowAdaptiveInfo = windowAdaptiveInfo, + ) + } } } } @@ -126,19 +136,18 @@ fun NiaApp( @OptIn( ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class, + ExperimentalMaterial3AdaptiveApi::class, ) internal fun NiaApp( appState: NiaAppState, - snackbarHostState: SnackbarHostState, showSettingsDialog: Boolean, onSettingsDismissed: () -> Unit, onTopAppBarActionClick: () -> Unit, modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { - val unreadDestinations by appState.topLevelDestinationsWithUnreadResources + val unreadNavKeys by appState.topLevelNavKeysWithUnreadResources .collectAsStateWithLifecycle() - val currentDestination = appState.currentDestination if (showSettingsDialog) { SettingsDialog( @@ -146,28 +155,31 @@ internal fun NiaApp( ) } + val snackbarHostState = LocalSnackbarHostState.current + + val navigator = remember { Navigator(appState.navigationState) } + NiaNavigationSuiteScaffold( navigationSuiteItems = { - appState.topLevelDestinations.forEach { destination -> - val hasUnread = unreadDestinations.contains(destination) - val selected = currentDestination - .isRouteInHierarchy(destination.baseRoute) + TOP_LEVEL_NAV_ITEMS.forEach { (navKey, navItem) -> + val hasUnread = unreadNavKeys.contains(navKey) + val selected = navKey == appState.navigationState.currentTopLevelKey item( selected = selected, - onClick = { appState.navigateToTopLevelDestination(destination) }, + onClick = { navigator.navigate(navKey) }, icon = { Icon( - imageVector = destination.unselectedIcon, + imageVector = navItem.unselectedIcon, contentDescription = null, ) }, selectedIcon = { Icon( - imageVector = destination.selectedIcon, + imageVector = navItem.selectedIcon, contentDescription = null, ) }, - label = { Text(stringResource(destination.iconTextId)) }, + label = { Text(stringResource(navItem.iconTextId)) }, modifier = Modifier .testTag("NiaNavItem") .then(if (hasUnread) Modifier.notificationDot() else Modifier), @@ -205,27 +217,30 @@ internal fun NiaApp( ), ), ) { - // Show the top app bar on top level destinations. - val destination = appState.currentTopLevelDestination + // Only show the top app bar on top level destinations. var shouldShowTopAppBar = false - if (destination != null) { + if (appState.navigationState.currentKey in appState.navigationState.topLevelKeys) { shouldShowTopAppBar = true + + val destination = TOP_LEVEL_NAV_ITEMS[appState.navigationState.currentTopLevelKey] + ?: error("Top level nav item not found for ${appState.navigationState.currentTopLevelKey}") + NiaTopAppBar( titleRes = destination.titleTextId, navigationIcon = NiaIcons.Search, navigationIconContentDescription = stringResource( - id = settingsR.string.feature_settings_top_app_bar_navigation_icon_description, + id = settingsR.string.feature_settings_impl_top_app_bar_navigation_icon_description, ), actionIcon = NiaIcons.Settings, actionIconContentDescription = stringResource( - id = settingsR.string.feature_settings_top_app_bar_action_icon_description, + id = settingsR.string.feature_settings_impl_top_app_bar_action_icon_description, ), colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent, ), onActionClick = { onTopAppBarActionClick() }, - onNavigationClick = { appState.navigateToSearch() }, + onNavigationClick = { navigator.navigate(SearchNavKey) }, ) } @@ -239,15 +254,20 @@ internal fun NiaApp( }, ), ) { - NiaNavHost( - appState = appState, - onShowSnackbar = { message, action -> - snackbarHostState.showSnackbar( - message = message, - actionLabel = action, - duration = Short, - ) == ActionPerformed - }, + val listDetailStrategy = rememberListDetailSceneStrategy() + + val entryProvider = entryProvider { + forYouEntry(navigator) + bookmarksEntry(navigator) + interestsEntry(navigator) + topicEntry(navigator) + searchEntry(navigator) + } + + NavDisplay( + entries = appState.navigationState.toEntries(entryProvider), + sceneStrategy = listDetailStrategy, + onBack = { navigator.goBack() }, ) } @@ -276,8 +296,3 @@ private fun Modifier.notificationDot(): Modifier = ) } } - -private fun NavDestination?.isRouteInHierarchy(route: KClass<*>) = - this?.hierarchy?.any { - it.hasRoute(route) - } ?: false 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 7c892c854..9b4814980 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 @@ -18,30 +18,18 @@ package com.google.samples.apps.nowinandroid.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.navigation.NavController -import androidx.navigation.NavDestination -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navOptions -import androidx.tracing.trace +import androidx.navigation3.runtime.NavKey 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.NavigationState +import com.google.samples.apps.nowinandroid.core.navigation.rememberNavigationState import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank -import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou -import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests -import com.google.samples.apps.nowinandroid.feature.search.navigation.navigateToSearch -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 -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey +import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey +import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -56,18 +44,20 @@ fun rememberNiaAppState( userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, coroutineScope: CoroutineScope = rememberCoroutineScope(), - navController: NavHostController = rememberNavController(), ): NiaAppState { - NavigationTrackingSideEffect(navController) + val navigationState = rememberNavigationState(ForYouNavKey, TOP_LEVEL_NAV_ITEMS.keys) + + NavigationTrackingSideEffect(navigationState) + return remember( - navController, + navigationState, coroutineScope, networkMonitor, userNewsResourceRepository, timeZoneMonitor, ) { NiaAppState( - navController = navController, + navigationState = navigationState, coroutineScope = coroutineScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, @@ -78,35 +68,12 @@ fun rememberNiaAppState( @Stable class NiaAppState( - val navController: NavHostController, + val navigationState: NavigationState, coroutineScope: CoroutineScope, networkMonitor: NetworkMonitor, userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, ) { - private val previousDestination = mutableStateOf(null) - - val currentDestination: NavDestination? - @Composable get() { - // Collect the currentBackStackEntryFlow as a state - val currentEntry = navController.currentBackStackEntryFlow - .collectAsState(initial = null) - - // Fallback to previousDestination if currentEntry is null - return currentEntry.value?.destination.also { destination -> - if (destination != null) { - previousDestination.value = destination - } - } ?: previousDestination.value - } - - val currentTopLevelDestination: TopLevelDestination? - @Composable get() { - return TopLevelDestination.entries.firstOrNull { topLevelDestination -> - currentDestination?.hasRoute(route = topLevelDestination.route) == true - } - } - val isOffline = networkMonitor.isOnline .map(Boolean::not) .stateIn( @@ -116,20 +83,14 @@ class NiaAppState( ) /** - * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the - * route. - */ - val topLevelDestinations: List = TopLevelDestination.entries - - /** - * The top level destinations that have unread news resources. + * The top level nav keys that have unread news resources. */ - val topLevelDestinationsWithUnreadResources: StateFlow> = + val topLevelNavKeysWithUnreadResources: StateFlow> = userNewsResourceRepository.observeAllForFollowedTopics() .combine(userNewsResourceRepository.observeAllBookmarked()) { forYouNewsResources, bookmarkedNewsResources -> setOfNotNull( - FOR_YOU.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, - BOOKMARKS.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, + ForYouNavKey.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, + BookmarksNavKey.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, ) } .stateIn( @@ -144,55 +105,15 @@ class NiaAppState( SharingStarted.WhileSubscribed(5_000), TimeZone.currentSystemDefault(), ) - - /** - * UI logic for navigating to a top level destination in the app. Top level destinations have - * only one copy of the destination of the back stack, and save and restore state whenever you - * navigate to and from it. - * - * @param topLevelDestination: The destination the app needs to navigate to. - */ - fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) { - trace("Navigation: ${topLevelDestination.name}") { - val topLevelNavOptions = navOptions { - // Pop up to the start destination of the graph to - // avoid building up a large stack of destinations - // on the back stack as users select items - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - // Avoid multiple copies of the same destination when - // reselecting the same item - launchSingleTop = true - // Restore state when reselecting a previously selected item - restoreState = true - } - - when (topLevelDestination) { - FOR_YOU -> navController.navigateToForYou(topLevelNavOptions) - BOOKMARKS -> navController.navigateToBookmarks(topLevelNavOptions) - INTERESTS -> navController.navigateToInterests(null, topLevelNavOptions) - } - } - } - - fun navigateToSearch() = navController.navigateToSearch() } /** * Stores information about navigation events to be used with JankStats */ @Composable -private fun NavigationTrackingSideEffect(navController: NavHostController) { - TrackDisposableJank(navController) { metricsHolder -> - val listener = NavController.OnDestinationChangedListener { _, destination, _ -> - metricsHolder.state?.putState("Navigation", destination.route.toString()) - } - - navController.addOnDestinationChangedListener(listener) - - onDispose { - navController.removeOnDestinationChangedListener(listener) - } +private fun NavigationTrackingSideEffect(navigationState: NavigationState) { + TrackDisposableJank(navigationState.currentKey) { metricsHolder -> + metricsHolder.state?.putState("Navigation", navigationState.currentKey.toString()) + onDispose {} } } diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt deleted file mode 100644 index 3d37f3417..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2024 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.ui.interests2pane - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.navigation.toRoute -import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject - -const val TOPIC_ID_KEY = "selectedTopicId" - -@HiltViewModel -class Interests2PaneViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, -) : ViewModel() { - - val route = savedStateHandle.toRoute() - val selectedTopicId: StateFlow = savedStateHandle.getStateFlow( - key = TOPIC_ID_KEY, - initialValue = route.initialTopicId, - ) - - fun onTopicClick(topicId: String?) { - savedStateHandle[TOPIC_ID_KEY] = topicId - } -} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt deleted file mode 100644 index c0f425c65..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright 2024 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.ui.interests2pane - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.LocalMinimumInteractiveComponentSize -import androidx.compose.material3.VerticalDragHandle -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.WindowAdaptiveInfo -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole -import androidx.compose.material3.adaptive.layout.PaneAdaptedValue -import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor -import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem -import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective -import androidx.compose.material3.adaptive.layout.defaultDragHandleSemantics -import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState -import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior -import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold -import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator -import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldPredictiveBackHandler -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.layout.layout -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute -import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute -import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder -import com.google.samples.apps.nowinandroid.feature.topic.TopicScreen -import com.google.samples.apps.nowinandroid.feature.topic.TopicViewModel -import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -import kotlin.math.max - -@Serializable internal object TopicPlaceholderRoute - -fun NavGraphBuilder.interestsListDetailScreen() { - composable { - InterestsListDetailScreen() - } -} - -@Composable -internal fun InterestsListDetailScreen( - viewModel: Interests2PaneViewModel = hiltViewModel(), - windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), -) { - val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle() - InterestsListDetailScreen( - selectedTopicId = selectedTopicId, - onTopicClick = viewModel::onTopicClick, - windowAdaptiveInfo = windowAdaptiveInfo, - ) -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -internal fun InterestsListDetailScreen( - selectedTopicId: String?, - onTopicClick: (String) -> Unit, - windowAdaptiveInfo: WindowAdaptiveInfo, -) { - val listDetailNavigator = rememberListDetailPaneScaffoldNavigator( - scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo), - initialDestinationHistory = listOfNotNull( - ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List), - ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.Detail).takeIf { - selectedTopicId != null - }, - ), - ) - val coroutineScope = rememberCoroutineScope() - - val paneExpansionState = rememberPaneExpansionState( - anchors = listOf( - PaneExpansionAnchor.Proportion(0f), - PaneExpansionAnchor.Proportion(0.5f), - PaneExpansionAnchor.Proportion(1f), - ), - ) - - ThreePaneScaffoldPredictiveBackHandler( - listDetailNavigator, - BackNavigationBehavior.PopUntilScaffoldValueChange, - ) - BackHandler( - paneExpansionState.currentAnchor == PaneExpansionAnchor.Proportion(0f) && - listDetailNavigator.isListPaneVisible() && - listDetailNavigator.isDetailPaneVisible(), - ) { - coroutineScope.launch { - paneExpansionState.animateTo(PaneExpansionAnchor.Proportion(1f)) - } - } - - var topicRoute by remember { - val route = selectedTopicId?.let { TopicRoute(id = it) } ?: TopicPlaceholderRoute - mutableStateOf(route) - } - - fun onTopicClickShowDetailPane(topicId: String) { - onTopicClick(topicId) - topicRoute = TopicRoute(id = topicId) - coroutineScope.launch { - listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail) - } - if (paneExpansionState.currentAnchor == PaneExpansionAnchor.Proportion(1f)) { - coroutineScope.launch { - paneExpansionState.animateTo(PaneExpansionAnchor.Proportion(0f)) - } - } - } - - val mutableInteractionSource = remember { MutableInteractionSource() } - val minPaneWidth = 300.dp - - NavigableListDetailPaneScaffold( - navigator = listDetailNavigator, - listPane = { - AnimatedPane { - Box( - modifier = Modifier.clipToBounds() - .layout { measurable, constraints -> - val width = max(minPaneWidth.roundToPx(), constraints.maxWidth) - val placeable = measurable.measure( - constraints.copy( - minWidth = minPaneWidth.roundToPx(), - maxWidth = width, - ), - ) - layout(constraints.maxWidth, placeable.height) { - placeable.placeRelative( - x = 0, - y = 0, - ) - } - }, - ) { - InterestsRoute( - onTopicClick = ::onTopicClickShowDetailPane, - shouldHighlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(), - ) - } - } - }, - detailPane = { - AnimatedPane { - Box( - modifier = Modifier.clipToBounds() - .layout { measurable, constraints -> - val width = max(minPaneWidth.roundToPx(), constraints.maxWidth) - val placeable = measurable.measure( - constraints.copy( - minWidth = minPaneWidth.roundToPx(), - maxWidth = width, - ), - ) - layout(constraints.maxWidth, placeable.height) { - placeable.placeRelative( - x = constraints.maxWidth - - max(constraints.maxWidth, placeable.width), - y = 0, - ) - } - }, - ) { - AnimatedContent(topicRoute) { route -> - when (route) { - is TopicRoute -> { - TopicScreen( - showBackButton = !listDetailNavigator.isListPaneVisible(), - onBackClick = { - coroutineScope.launch { - listDetailNavigator.navigateBack() - } - }, - onTopicClick = ::onTopicClickShowDetailPane, - viewModel = hiltViewModel( - key = route.id, - ) { factory -> - factory.create(route.id) - }, - ) - } - is TopicPlaceholderRoute -> { - TopicDetailPlaceholder() - } - } - } - } - } - }, - paneExpansionState = paneExpansionState, - paneExpansionDragHandle = { - VerticalDragHandle( - modifier = Modifier.paneExpansionDraggable( - state = paneExpansionState, - minTouchTargetSize = LocalMinimumInteractiveComponentSize.current, - interactionSource = mutableInteractionSource, - semanticsProperties = paneExpansionState.defaultDragHandleSemantics(), - ), - interactionSource = mutableInteractionSource, - ) - }, - ) -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -private fun ThreePaneScaffoldNavigator.isListPaneVisible(): Boolean = - scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -private fun ThreePaneScaffoldNavigator.isDetailPaneVisible(): Boolean = - scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded 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 c6ddb54fb..810b77cf0 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 @@ -16,21 +16,19 @@ package com.google.samples.apps.nowinandroid.ui -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.junit4.createComposeRule -import androidx.navigation.NavHostController -import androidx.navigation.compose.ComposeNavigator -import androidx.navigation.compose.composable -import androidx.navigation.createGraph -import androidx.navigation.testing.TestNavHostController +import androidx.navigation3.runtime.NavBackStack import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository +import com.google.samples.apps.nowinandroid.core.navigation.NavigationState +import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey +import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication import kotlinx.coroutines.flow.collect @@ -44,7 +42,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import kotlin.test.assertEquals -import kotlin.test.assertTrue /** * Tests [NiaAppState]. @@ -68,32 +65,42 @@ class NiaAppStateTest { // Subject under test. private lateinit var state: NiaAppState + private fun testNavigationState() = NavigationState( + startKey = ForYouNavKey, + topLevelStack = NavBackStack(ForYouNavKey), + subStacks = mapOf( + ForYouNavKey to NavBackStack(ForYouNavKey), + BookmarksNavKey to NavBackStack(BookmarksNavKey), + ), + ) + @Test fun niaAppState_currentDestination() = runTest { - var currentDestination: String? = null + val navigationState = testNavigationState() + val navigator = Navigator(navigationState) composeTestRule.setContent { - val navController = rememberTestNavController() - state = remember(navController) { + state = remember(navigationState) { NiaAppState( - navController = navController, coroutineScope = backgroundScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, + navigationState = navigationState, ) } + } - // Update currentDestination whenever it changes - currentDestination = state.currentDestination?.route + assertEquals(ForYouNavKey, state.navigationState.currentTopLevelKey) + assertEquals(ForYouNavKey, state.navigationState.currentKey) - // Navigate to destination b once - LaunchedEffect(Unit) { - navController.setCurrentDestination("b") - } - } + // Navigate to another destination once + navigator.navigate(BookmarksNavKey) - assertEquals("b", currentDestination) + composeTestRule.waitForIdle() + + assertEquals(BookmarksNavKey, state.navigationState.currentTopLevelKey) + assertEquals(BookmarksNavKey, state.navigationState.currentKey) } @Test @@ -106,21 +113,24 @@ class NiaAppStateTest { ) } - assertEquals(3, state.topLevelDestinations.size) - assertTrue(state.topLevelDestinations[0].name.contains("for_you", true)) - assertTrue(state.topLevelDestinations[1].name.contains("bookmarks", true)) - assertTrue(state.topLevelDestinations[2].name.contains("interests", true)) + val navigationState = state.navigationState + + assertEquals(3, navigationState.topLevelKeys.size) + assertEquals( + setOf(ForYouNavKey, BookmarksNavKey, InterestsNavKey(null)), + navigationState.topLevelKeys, + ) } @Test fun niaAppState_whenNetworkMonitorIsOffline_StateIsOffline() = runTest(UnconfinedTestDispatcher()) { composeTestRule.setContent { state = NiaAppState( - navController = NavHostController(LocalContext.current), coroutineScope = backgroundScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, + navigationState = testNavigationState(), ) } @@ -136,11 +146,11 @@ class NiaAppStateTest { fun niaAppState_differentTZ_withTimeZoneMonitorChange() = runTest(UnconfinedTestDispatcher()) { composeTestRule.setContent { state = NiaAppState( - navController = NavHostController(LocalContext.current), coroutineScope = backgroundScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, + navigationState = testNavigationState(), ) } val changedTz = TimeZone.of("Europe/Prague") @@ -152,18 +162,3 @@ class NiaAppStateTest { ) } } - -@Composable -private fun rememberTestNavController(): TestNavHostController { - val context = LocalContext.current - return remember { - TestNavHostController(context).apply { - navigatorProvider.addNavigator(ComposeNavigator()) - graph = createGraph(startDestination = "a") { - composable("a") { } - composable("b") { } - composable("c") { } - } - } - } -} 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 78f568e03..3c7610193 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 @@ -67,6 +67,7 @@ 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.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -147,9 +148,7 @@ class SnackbarInsetsScreenshotTests { @Test fun phone_noSnackbar() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 400.dp, 500.dp, "insets_snackbar_compact_medium_noSnackbar", @@ -159,13 +158,11 @@ class SnackbarInsetsScreenshotTests { @Test fun snackbarShown_phone() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 400.dp, 500.dp, "insets_snackbar_compact_medium", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -176,13 +173,11 @@ class SnackbarInsetsScreenshotTests { @Test fun snackbarShown_foldable() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 600.dp, 600.dp, "insets_snackbar_medium_medium", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -193,13 +188,11 @@ class SnackbarInsetsScreenshotTests { @Test fun snackbarShown_tablet() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 900.dp, 900.dp, "insets_snackbar_expanded_expanded", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -209,17 +202,18 @@ class SnackbarInsetsScreenshotTests { } private fun testSnackbarScreenshotWithSize( - snackbarHostState: SnackbarHostState, width: Dp, height: Dp, screenshotName: String, - action: suspend () -> Unit, + action: suspend (snackbarHostState: SnackbarHostState) -> Unit, ) { lateinit var scope: CoroutineScope + val snackbarHostState = SnackbarHostState() composeTestRule.setContent { CompositionLocalProvider( // Replaces images with placeholders LocalInspectionMode provides true, + LocalSnackbarHostState provides snackbarHostState, ) { scope = rememberCoroutineScope() @@ -259,7 +253,6 @@ class SnackbarInsetsScreenshotTests { ) NiaApp( appState = appState, - snackbarHostState = snackbarHostState, showSettingsDialog = false, onSettingsDismissed = {}, onTopAppBarActionClick = {}, @@ -280,7 +273,7 @@ class SnackbarInsetsScreenshotTests { } scope.launch { - action() + action(snackbarHostState) } composeTestRule.onNodeWithTag("root") 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 b9b1047c1..75dc6baa7 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 @@ -40,6 +40,7 @@ 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.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -120,9 +121,7 @@ class SnackbarScreenshotTests { @Test fun phone_noSnackbar() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 400.dp, 500.dp, "snackbar_compact_medium_noSnackbar", @@ -132,13 +131,11 @@ class SnackbarScreenshotTests { @Test fun snackbarShown_phone() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 400.dp, 500.dp, "snackbar_compact_medium", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -149,13 +146,11 @@ class SnackbarScreenshotTests { @Test fun snackbarShown_foldable() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 600.dp, 600.dp, "snackbar_medium_medium", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -166,13 +161,11 @@ class SnackbarScreenshotTests { @Test fun snackbarShown_tablet() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 900.dp, 900.dp, "snackbar_expanded_expanded", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -182,17 +175,19 @@ class SnackbarScreenshotTests { } private fun testSnackbarScreenshotWithSize( - snackbarHostState: SnackbarHostState, width: Dp, height: Dp, screenshotName: String, - action: suspend () -> Unit, + action: suspend (snackbarHostState: SnackbarHostState) -> Unit, ) { lateinit var scope: CoroutineScope + val snackbarHostState = SnackbarHostState() composeTestRule.setContent { CompositionLocalProvider( // Replaces images with placeholders LocalInspectionMode provides true, + LocalSnackbarHostState provides snackbarHostState, + ) { scope = rememberCoroutineScope() @@ -208,7 +203,6 @@ class SnackbarScreenshotTests { ) NiaApp( appState = appState, - snackbarHostState = snackbarHostState, showSettingsDialog = false, onSettingsDismissed = {}, onTopAppBarActionClick = {}, @@ -227,7 +221,7 @@ class SnackbarScreenshotTests { } scope.launch { - action() + action(snackbarHostState) } composeTestRule.onRoot() diff --git a/app/src/testDemo/resources/robolectric.properties b/app/src/testDemo/resources/robolectric.properties new file mode 100644 index 000000000..ca82be153 --- /dev/null +++ b/app/src/testDemo/resources/robolectric.properties @@ -0,0 +1,17 @@ +# +# 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. +# + +sdk = 35 \ No newline at end of file diff --git a/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png index 7a49b2c32..a001de939 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png index ab20746cb..d13212045 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png index 926ce00f1..309fd305f 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png index d793bed51..78d792c9e 100644 Binary files a/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png index b6f89e516..77dc72def 100644 Binary files a/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png index 2bf46ca4f..84e59fabe 100644 Binary files a/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png b/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png index db6488e34..cfbbc455e 100644 Binary files a/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png and b/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png index becfe5715..c2a85bf1f 100644 Binary files a/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png and b/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png b/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png index 578ff0b6d..97bd7919b 100644 Binary files a/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png and b/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png index 64cf8f32f..74f8d2862 100644 Binary files a/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png index 679846fc1..e5f3f47ec 100644 Binary files a/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png index b8ddeb12b..77ed5f958 100644 Binary files a/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/snackbar_compact_medium.png b/app/src/testDemo/screenshots/snackbar_compact_medium.png index ad12a4173..0e8da24ca 100644 Binary files a/app/src/testDemo/screenshots/snackbar_compact_medium.png and b/app/src/testDemo/screenshots/snackbar_compact_medium.png differ diff --git a/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png b/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png index bc466edcb..317797542 100644 Binary files a/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png and b/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png differ diff --git a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png index fc98037a6..f324fb9fe 100644 Binary files a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png and b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png differ diff --git a/app/src/testDemo/screenshots/snackbar_medium_medium.png b/app/src/testDemo/screenshots/snackbar_medium_medium.png index 1dfc73e5a..7921bbbbf 100644 Binary files a/app/src/testDemo/screenshots/snackbar_medium_medium.png and b/app/src/testDemo/screenshots/snackbar_medium_medium.png differ diff --git a/benchmarks/README.md b/benchmarks/README.md index 657ad5422..c2bbf2a2a 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -11,6 +11,42 @@ config: nodePlacementStrategy: SIMPLE --- graph TB + subgraph :feature + direction TB + subgraph :feature:settings + direction TB + :feature:settings:impl[impl]:::android-library + end + subgraph :feature:foryou + direction TB + :feature:foryou:api[api]:::android-library + :feature:foryou:impl[impl]:::android-library + end + subgraph :feature:bookmarks + direction TB + :feature:bookmarks:api[api]:::android-library + :feature:bookmarks:impl[impl]:::android-library + end + subgraph :feature:search + direction TB + :feature:search:api[api]:::android-library + :feature:search:impl[impl]:::android-library + end + subgraph :feature:interests + direction TB + :feature:interests:api[api]:::android-library + :feature:interests:impl[impl]:::android-library + end + subgraph :feature:topic + direction TB + :feature:topic:api[api]:::android-library + :feature:topic:impl[impl]:::android-library + end + end + subgraph :sync + direction TB + :sync:work[work]:::android-library + end subgraph :core direction TB :core:analytics[analytics]:::android-library @@ -22,23 +58,11 @@ graph TB :core:designsystem[designsystem]:::android-library :core:domain[domain]:::android-library :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library :core:network[network]:::android-library :core:notifications[notifications]:::android-library :core:ui[ui]:::android-library end - subgraph :feature - direction TB - :feature:bookmarks[bookmarks]:::android-feature - :feature:foryou[foryou]:::android-feature - :feature:interests[interests]:::android-feature - :feature:search[search]:::android-feature - :feature:settings[settings]:::android-feature - :feature:topic[topic]:::android-feature - end - subgraph :sync - direction TB - :sync:work[work]:::android-library - end :benchmarks[benchmarks]:::android-test :app[app]:::android-application @@ -49,12 +73,17 @@ graph TB :app -.-> :core:designsystem :app -.-> :core:model :app -.-> :core:ui - :app -.-> :feature:bookmarks - :app -.-> :feature:foryou - :app -.-> :feature:interests - :app -.-> :feature:search - :app -.-> :feature:settings - :app -.-> :feature:topic + :app -.-> :feature:bookmarks:api + :app -.-> :feature:bookmarks:impl + :app -.-> :feature:foryou:api + :app -.-> :feature:foryou:impl + :app -.-> :feature:interests:api + :app -.-> :feature:interests:impl + :app -.-> :feature:search:api + :app -.-> :feature:search:impl + :app -.-> :feature:settings:impl + :app -.-> :feature:topic:api + :app -.-> :feature:topic:impl :app -.-> :sync:work :benchmarks -.->|testedApks| :app :core:data -.-> :core:analytics @@ -76,28 +105,43 @@ graph TB :core:ui --> :core:analytics :core:ui --> :core:designsystem :core:ui --> :core:model - :feature:bookmarks -.-> :core:data - :feature:bookmarks -.-> :core:designsystem - :feature:bookmarks -.-> :core:ui - :feature:foryou -.-> :core:data - :feature:foryou -.-> :core:designsystem - :feature:foryou -.-> :core:domain - :feature:foryou -.-> :core:notifications - :feature:foryou -.-> :core:ui - :feature:interests -.-> :core:data - :feature:interests -.-> :core:designsystem - :feature:interests -.-> :core:domain - :feature:interests -.-> :core:ui - :feature:search -.-> :core:data - :feature:search -.-> :core:designsystem - :feature:search -.-> :core:domain - :feature:search -.-> :core:ui - :feature:settings -.-> :core:data - :feature:settings -.-> :core:designsystem - :feature:settings -.-> :core:ui - :feature:topic -.-> :core:data - :feature:topic -.-> :core:designsystem - :feature:topic -.-> :core:ui + :feature:bookmarks:api --> :core:navigation + :feature:bookmarks:impl -.-> :core:data + :feature:bookmarks:impl -.-> :core:designsystem + :feature:bookmarks:impl -.-> :core:ui + :feature:bookmarks:impl -.-> :feature:bookmarks:api + :feature:bookmarks:impl -.-> :feature:topic:api + :feature:foryou:api --> :core:navigation + :feature:foryou:impl -.-> :core:designsystem + :feature:foryou:impl -.-> :core:domain + :feature:foryou:impl -.-> :core:notifications + :feature:foryou:impl -.-> :core:ui + :feature:foryou:impl -.-> :feature:foryou:api + :feature:foryou:impl -.-> :feature:topic:api + :feature:interests:api --> :core:navigation + :feature:interests:impl -.-> :core:designsystem + :feature:interests:impl -.-> :core:domain + :feature:interests:impl -.-> :core:ui + :feature:interests:impl -.-> :feature:interests:api + :feature:interests:impl -.-> :feature:topic:api + :feature:search:api -.-> :core:domain + :feature:search:api --> :core:navigation + :feature:search:impl -.-> :core:designsystem + :feature:search:impl -.-> :core:domain + :feature:search:impl -.-> :core:ui + :feature:search:impl -.-> :feature:interests:api + :feature:search:impl -.-> :feature:search:api + :feature:search:impl -.-> :feature:topic:api + :feature:settings:impl -.-> :core:data + :feature:settings:impl -.-> :core:designsystem + :feature:settings:impl -.-> :core:ui + :feature:topic:api -.-> :core:designsystem + :feature:topic:api --> :core:navigation + :feature:topic:api -.-> :core:ui + :feature:topic:impl -.-> :core:data + :feature:topic:impl -.-> :core:designsystem + :feature:topic:impl -.-> :core:ui + :feature:topic:impl -.-> :feature:topic:api :sync:work -.-> :core:analytics :sync:work -.-> :core:data :sync:work -.-> :core:notifications diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 148a1eab6..5faf257c1 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -78,9 +78,13 @@ gradlePlugin { id = libs.plugins.nowinandroid.android.library.asProvider().get().pluginId implementationClass = "AndroidLibraryConventionPlugin" } - register("androidFeature") { - id = libs.plugins.nowinandroid.android.feature.get().pluginId - implementationClass = "AndroidFeatureConventionPlugin" + register("androidFeatureImpl") { + id = libs.plugins.nowinandroid.android.feature.impl.get().pluginId + implementationClass = "AndroidFeatureImplConventionPlugin" + } + register("androidFeatureApi") { + id = libs.plugins.nowinandroid.android.feature.api.get().pluginId + implementationClass = "AndroidFeatureApiConventionPlugin" } register("androidLibraryJacoco") { id = libs.plugins.nowinandroid.android.library.jacoco.get().pluginId diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt index a8b1b1779..0a33b719f 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -31,5 +31,4 @@ class AndroidApplicationComposeConventionPlugin : Plugin { configureAndroidCompose(extension) } } - } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index e952a8f5c..684821bfc 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -36,7 +36,7 @@ class AndroidApplicationConventionPlugin : Plugin { extensions.configure { configureKotlinAndroid(this) - defaultConfig.targetSdk = 35 + defaultConfig.targetSdk = 36 @Suppress("UnstableApiUsage") testOptions.animationsDisabled = true configureGradleManagedDevices(this) diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureApiConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureApiConventionPlugin.kt new file mode 100644 index 000000000..969cf96d4 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureApiConventionPlugin.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2022 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. + */ + +import com.google.samples.apps.nowinandroid.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.dependencies + +class AndroidFeatureApiConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + apply(plugin = "nowinandroid.android.library") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") + + dependencies { + "api"(project(":core:navigation")) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureImplConventionPlugin.kt similarity index 85% rename from build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt rename to build-logic/convention/src/main/kotlin/AndroidFeatureImplConventionPlugin.kt index 343bd4859..500e3e983 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureImplConventionPlugin.kt @@ -23,12 +23,11 @@ import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies -class AndroidFeatureConventionPlugin : Plugin { +class AndroidFeatureImplConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { apply(plugin = "nowinandroid.android.library") apply(plugin = "nowinandroid.hilt") - apply(plugin = "org.jetbrains.kotlin.plugin.serialization") extensions.configure { testOptions.animationsDisabled = true @@ -39,14 +38,12 @@ class AndroidFeatureConventionPlugin : Plugin { "implementation"(project(":core:ui")) "implementation"(project(":core:designsystem")) - "implementation"(libs.findLibrary("androidx.hilt.navigation.compose").get()) "implementation"(libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) "implementation"(libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) - "implementation"(libs.findLibrary("androidx.navigation.compose").get()) + "implementation"(libs.findLibrary("androidx.hilt.lifecycle.viewModelCompose").get()) + "implementation"(libs.findLibrary("androidx.navigation3.runtime").get()) "implementation"(libs.findLibrary("androidx.tracing.ktx").get()) - "implementation"(libs.findLibrary("kotlinx.serialization.json").get()) - "testImplementation"(libs.findLibrary("androidx.navigation.testing").get()) "androidTestImplementation"( libs.findLibrary("androidx.lifecycle.runtimeTesting").get(), ) diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 63a992b05..18cd2bd7d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -31,5 +31,4 @@ class AndroidLibraryComposeConventionPlugin : Plugin { configureAndroidCompose(extension) } } - } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 17c4379e7..ee47e823a 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -36,7 +36,8 @@ class AndroidLibraryConventionPlugin : Plugin { extensions.configure { configureKotlinAndroid(this) - lint.targetSdk = 35 + testOptions.targetSdk = 36 + lint.targetSdk = 36 defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testOptions.animationsDisabled = true configureFlavors(this) diff --git a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt index ddfd03e24..8a2f0b5a4 100644 --- a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt @@ -29,7 +29,7 @@ class AndroidTestConventionPlugin : Plugin { extensions.configure { configureKotlinAndroid(this) - defaultConfig.targetSdk = 35 + defaultConfig.targetSdk = 36 configureGradleManagedDevices(this) } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt index f018cbfc5..8aa8a48e8 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt @@ -19,7 +19,6 @@ package com.google.samples.apps.nowinandroid import com.android.build.api.dsl.CommonExtension import org.gradle.api.Project import org.gradle.api.provider.Provider -import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Graph.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Graph.kt index 439a50181..80448e2b2 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Graph.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Graph.kt @@ -187,20 +187,46 @@ private abstract class GraphDumpTask : DefaultTask() { ) // Graph declaration appendLine("graph TB") - // Nodes and subgraphs (limited to a single nested layer) + // Nodes and subgraphs val (rootProjects, nestedProjects) = dependencies .map { listOf(it.project, it.dependency) }.flatten().toSet() .plus(projectPath.get()) // Special case when this specific module has no other dependency .groupBy { it.substringBeforeLast(":") } .entries.partition { it.key.isEmpty() } - nestedProjects.sortedByDescending { it.value.size }.forEach { (group, projects) -> - appendLine(" subgraph $group") - appendLine(" direction TB") - projects.sorted().forEach { - appendLine(it.alias(indent = 4, plugins.get().getValue(it))) + + val orderedGroups = nestedProjects.groupBy { + if (it.key.count { char -> char == ':' } > 1) it.key.substringBeforeLast(":") else "" + } + + orderedGroups.forEach { (outerGroup, innerGroups) -> + if (outerGroup.isNotEmpty()) { + appendLine(" subgraph $outerGroup") + appendLine(" direction TB") + } + innerGroups.sortedWith( + compareBy( + { (group, _) -> + dependencies.filter { dep -> + val toGroup = dep.dependency.substringBeforeLast(":") + toGroup == group && dep.project.substringBeforeLast(":") != group + }.count() + }, + { -it.value.size }, + ), + ).forEach { (group, projects) -> + val indent = if (outerGroup.isNotEmpty()) 4 else 2 + appendLine(" ".repeat(indent) + "subgraph $group") + appendLine(" ".repeat(indent) + " direction TB") + projects.sorted().forEach { + appendLine(it.alias(indent = indent + 2, plugins.get().getValue(it))) + } + appendLine(" ".repeat(indent) + "end") + } + if (outerGroup.isNotEmpty()) { + appendLine(" end") } - appendLine(" end") } + rootProjects.flatMap { it.value }.sortedDescending().forEach { appendLine(it.alias(indent = 2, plugins.get().getValue(it))) } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt index a923ce24e..37b1ba99b 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt @@ -35,7 +35,7 @@ internal fun Project.configureKotlinAndroid( commonExtension: CommonExtension, ) { commonExtension.apply { - compileSdk = 35 + compileSdk = 36 defaultConfig.apply { minSdk = 23 diff --git a/core/designsystem/src/test/resources/robolectric.properties b/core/designsystem/src/test/resources/robolectric.properties new file mode 100644 index 000000000..ca82be153 --- /dev/null +++ b/core/designsystem/src/test/resources/robolectric.properties @@ -0,0 +1,17 @@ +# +# 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. +# + +sdk = 35 \ No newline at end of file diff --git a/feature/bookmarks/.gitignore b/core/navigation/.gitignore similarity index 100% rename from feature/bookmarks/.gitignore rename to core/navigation/.gitignore diff --git a/core/navigation/README.md b/core/navigation/README.md new file mode 100644 index 000000000..7cd3b7e9f --- /dev/null +++ b/core/navigation/README.md @@ -0,0 +1,3 @@ +# :core:navigation module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_navigation.svg) diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts new file mode 100644 index 000000000..3e7831dca --- /dev/null +++ b/core/navigation/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * 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. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.library) + alias(libs.plugins.nowinandroid.hilt) + alias(libs.plugins.hilt) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose) +} + +android { + namespace = "com.google.samples.apps.nowinandroid.core.navigation" +} + +dependencies { + api(libs.androidx.navigation3.runtime) + implementation(libs.androidx.savedstate.compose) + implementation(libs.androidx.lifecycle.viewModel.navigation3) + + testImplementation(libs.truth) + + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.test.ext) + androidTestImplementation(libs.androidx.compose.ui.testManifest) + androidTestImplementation(libs.androidx.lifecycle.viewModel.testing) + androidTestImplementation(libs.truth) +} diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigationState.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigationState.kt new file mode 100644 index 000000000..864fec794 --- /dev/null +++ b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigationState.kt @@ -0,0 +1,102 @@ +/* + * 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.annotation.VisibleForTesting +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.rememberDecoratedNavEntries +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator + +/** + * Create a navigation state that persists config changes and process death. + */ +@Composable +fun rememberNavigationState( + startKey: NavKey, + topLevelKeys: Set, +): NavigationState { + val topLevelStack = rememberNavBackStack(startKey) + val subStacks = topLevelKeys.associateWith { key -> rememberNavBackStack(key) } + + return remember(startKey, topLevelKeys) { + NavigationState( + startKey = startKey, + topLevelStack = topLevelStack, + subStacks = subStacks, + ) + } +} + +/** + * State holder for navigation state. + * + * @param startKey - the starting navigation key. The user will exit the app through this key. + * @param topLevelStack - the top level back stack. It holds only top level keys. + * @param subStacks - the back stacks for each top level key + */ +class NavigationState( + val startKey: NavKey, + val topLevelStack: NavBackStack, + val subStacks: Map>, +) { + val currentTopLevelKey: NavKey by derivedStateOf { topLevelStack.last() } + + val topLevelKeys + get() = subStacks.keys + + @get:VisibleForTesting + val currentSubStack: NavBackStack + get() = subStacks[currentTopLevelKey] + ?: error("Sub stack for $currentTopLevelKey does not exist") + + @get:VisibleForTesting + val currentKey: NavKey by derivedStateOf { currentSubStack.last() } +} + +/** + * Convert NavigationState into NavEntries. + */ +@Composable +fun NavigationState.toEntries( + entryProvider: (NavKey) -> NavEntry, +): SnapshotStateList> { + val decoratedEntries = subStacks.mapValues { (_, stack) -> + val decorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ) + rememberDecoratedNavEntries( + backStack = stack, + entryDecorators = decorators, + entryProvider = entryProvider, + ) + } + + return topLevelStack + .flatMap { decoratedEntries[it] ?: emptyList() } + .toMutableStateList() +} diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/Navigator.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/Navigator.kt new file mode 100644 index 000000000..c1c456476 --- /dev/null +++ b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/Navigator.kt @@ -0,0 +1,91 @@ +/* + * 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.navigation3.runtime.NavKey + +/** + * Handles navigation events (forward and back) by updating the navigation state. + * + * @param state - The navigation state that will be updated in response to navigation events. + */ +class Navigator(val state: NavigationState) { + + /** + * Navigate to a navigation key + * + * @param key - the navigation key to navigate to. + */ + fun navigate(key: NavKey) { + when (key) { + state.currentTopLevelKey -> clearSubStack() + in state.topLevelKeys -> goToTopLevel(key) + else -> goToKey(key) + } + } + + /** + * Go back to the previous navigation key. + */ + fun goBack() { + when (state.currentKey) { + state.startKey -> error("You cannot go back from the start route") + state.currentTopLevelKey -> { + // We're at the base of the current sub stack, go back to the previous top level + // stack. + state.topLevelStack.removeLastOrNull() + } + else -> state.currentSubStack.removeLastOrNull() + } + } + + /** + * Go to a non top level key. + */ + private fun goToKey(key: NavKey) { + state.currentSubStack.apply { + // Remove it if it's already in the stack so it's added at the end. + remove(key) + add(key) + } + } + + /** + * Go to a top level stack. + */ + private fun goToTopLevel(key: NavKey) { + state.topLevelStack.apply { + if (key == state.startKey) { + // This is the start key. Clear the stack so it's added as the only key. + clear() + } else { + // Remove it if it's already in the stack so it's added at the end. + remove(key) + } + add(key) + } + } + + /** + * Clearing all but the root key in the current sub stack. + */ + private fun clearSubStack() { + state.currentSubStack.run { + if (size > 1) subList(1, size).clear() + } + } +} diff --git a/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigatorTest.kt b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigatorTest.kt new file mode 100644 index 000000000..86c4acc25 --- /dev/null +++ b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigatorTest.kt @@ -0,0 +1,256 @@ +/* + * 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.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import kotlin.test.assertFailsWith + +private object TestFirstTopLevelKey : NavKey +private object TestSecondTopLevelKey : NavKey +private object TestThirdTopLevelKey : NavKey +private object TestKeyFirst : NavKey +private object TestKeySecond : NavKey + +class NavigatorTest { + + private lateinit var navigationState: NavigationState + private lateinit var navigator: Navigator + + @Before + fun setup() { + val startKey = TestFirstTopLevelKey + val topLevelStack = NavBackStack(startKey) + val topLevelKeys = listOf( + startKey, + TestSecondTopLevelKey, + TestThirdTopLevelKey, + ) + val subStacks = topLevelKeys.associateWith { key -> NavBackStack(key) } + + navigationState = NavigationState( + startKey = startKey, + topLevelStack = topLevelStack, + subStacks = subStacks, + ) + navigator = Navigator(navigationState) + } + + @Test + fun testStartKey() { + assertThat(navigationState.startKey).isEqualTo(TestFirstTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun testNavigate() { + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + assertThat(navigationState.subStacks[TestFirstTopLevelKey]?.last()).isEqualTo(TestKeyFirst) + } + + @Test + fun testNavigateTopLevel() { + navigator.navigate(TestSecondTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey) + } + + @Test + fun testNavigateSingleTop() { + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + ).inOrder() + + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + ).inOrder() + } + + @Test + fun testNavigateTopLevelSingleTop() { + navigator.navigate(TestSecondTopLevelKey) + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentSubStack).containsExactly( + TestSecondTopLevelKey, + TestKeyFirst, + ).inOrder() + + navigator.navigate(TestSecondTopLevelKey) + + assertThat(navigationState.currentSubStack).containsExactly( + TestSecondTopLevelKey, + ).inOrder() + } + + @Test + fun testSubStack() { + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + + navigator.navigate(TestKeySecond) + + assertThat(navigationState.currentKey).isEqualTo(TestKeySecond) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun testMultiStack() { + // add to start stack + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + + // navigate to new top level + navigator.navigate(TestSecondTopLevelKey) + + assertThat(navigationState.currentKey).isEqualTo(TestSecondTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey) + + // add to new stack + navigator.navigate(TestKeySecond) + + assertThat(navigationState.currentKey).isEqualTo(TestKeySecond) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey) + + // go back to start stack + navigator.navigate(TestFirstTopLevelKey) + + assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun testPopOneNonTopLevel() { + navigator.navigate(TestKeyFirst) + navigator.navigate(TestKeySecond) + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + TestKeySecond, + ).inOrder() + + navigator.goBack() + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + ).inOrder() + + assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun testPopOneTopLevel() { + navigator.navigate(TestKeyFirst) + navigator.navigate(TestSecondTopLevelKey) + + assertThat(navigationState.currentSubStack).containsExactly( + TestSecondTopLevelKey, + ).inOrder() + + assertThat(navigationState.currentKey).isEqualTo(TestSecondTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey) + + // remove TopLevel + navigator.goBack() + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + ).inOrder() + + assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun popMultipleNonTopLevel() { + navigator.navigate(TestKeyFirst) + navigator.navigate(TestKeySecond) + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + TestKeySecond, + ).inOrder() + + navigator.goBack() + navigator.goBack() + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + ).inOrder() + + assertThat(navigationState.currentKey).isEqualTo(TestFirstTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun popMultipleTopLevel() { + // second sub-stack + navigator.navigate(TestSecondTopLevelKey) + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentSubStack).containsExactly( + TestSecondTopLevelKey, + TestKeyFirst, + ).inOrder() + + // third sub-stack + navigator.navigate(TestThirdTopLevelKey) + navigator.navigate(TestKeySecond) + + assertThat(navigationState.currentSubStack).containsExactly( + TestThirdTopLevelKey, + TestKeySecond, + ).inOrder() + + repeat(4) { + navigator.goBack() + } + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + ).inOrder() + + assertThat(navigationState.currentKey).isEqualTo(TestFirstTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun throwOnEmptyBackStack() { + assertFailsWith { + navigator.goBack() + } + } +} diff --git a/docs/ModularizationLearningJourney.md b/docs/ModularizationLearningJourney.md index 5cda61b69..b50242776 100644 --- a/docs/ModularizationLearningJourney.md +++ b/docs/ModularizationLearningJourney.md @@ -1,89 +1,12 @@ # Modularization learning journey -In this learning journey you will learn about modularization, and the modularization strategy used -to create the modules in the Now in Android app. +In this learning journey you will learn about the modularization strategy used +to create modules in the Now in Android app. For the theory behind modularization, check out +[the official guidance](https://developer.android.com/topic/modularization). +**IMPORTANT:** Every module has a dependency graph in its README ([example for the app module](https://github.com/android/nowinandroid/tree/main/app)) which can be useful for understanding the overall structure of the project. -## Overview - -Modularization is the practice of breaking the concept of a monolithic, one-module codebase into -loosely coupled, self contained modules. - - -### Benefits of modularization - -This offers many benefits, including: - -**Scalability** - In a tightly coupled codebase, a single change can trigger a cascade of -alterations. A properly modularized project will embrace -the [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) principle. This -in turn empowers the contributors with more autonomy while also enforcing architectural patterns. - -**Enabling work in parallel** - Modularization helps decrease version control conflicts and enables -more efficient work in parallel for developers in larger teams. - -**Ownership** - A module can have a dedicated owner who is responsible for maintaining the code and -tests, fixing bugs, and reviewing changes. - -**Encapsulation** - Isolated code is easier to read, understand, test and maintain. - -**Reduced build time** - Leveraging Gradle’s parallel and incremental build can reduce build times. - -**Dynamic delivery** - Modularization is a requirement -for [Play Feature Delivery](https://developer.android.com/guide/playcore/feature-delivery) which -allows certain features of your app to be delivered conditionally or downloaded on demand. - -**Reusability** - Proper modularization enables opportunities for code sharing and building multiple -apps, across different platforms, from the same foundation. - - -### Modularization pitfalls - -However, modularization is a pattern that can be misused, and there are some gotchas to be aware of -when modularizing an app: - -**Too many modules** - each module has an overhead that comes in the form of increased complexity of -the build configuration. This can cause Gradle sync times to increase, and incurs an ongoing -maintenance cost. In addition, adding more modules increases the complexity of the project’s Gradle -setup, when compared to a single monolithic module. This can be mitigated by making use of -convention plugins, to extract reusable and composable build configuration into type-safe Kotlin -code. In the Now in Android app, these convention plugins can be found in -the [`build-logic` folder](https://github.com/android/nowinandroid/tree/main/build-logic). - -**Not enough modules** - conversely if your modules are few, large and tightly coupled, you end up -with yet another monolith. This means you lose some benefits of modularization. If your module is -bloated and has no single, well defined purpose, you should consider splitting it. - -**Too complex** - there is no silver bullet here. In fact it doesn’t always make sense to modularize -your project. A dominating factor is the size and relative complexity of the codebase. If your -project is not expected to grow beyond a certain threshold, the scalability and build time gains -won’t apply. - - -## Modularization strategy - -It’s important to note that there is no single modularization strategy that fits all projects. -However, there are general guidelines that can be followed to ensure you maximize its benefits and -minimize its downsides. - -A barebone module is simply a directory with a Gradle build script inside. Usually though, a module -will consist of one or more source sets and possibly a collection of resources or assets. Modules -can be built and tested independently. Due to Gradle's flexibility there are few constraints as to -how you can organize your project. In general, you should strive for low coupling and high cohesion. - -* **Low coupling** - Modules should be as independent as possible from one another, so that changes - to one module have zero or minimal impact on other modules. They should not possess knowledge of - the inner workings of other modules. - -* **High cohesion** - A module should comprise a collection of code that acts as a system. It should - have clearly defined responsibilities and stay within boundaries of certain domain knowledge. For - example, - the [`core:network` module](https://github.com/android/nowinandroid/tree/main/core/network) in Now - in Android is responsible for making network requests, handling responses from a remote data - source, and supplying data to other modules. - - -## Types of modules in Now in Android +## Module types ```mermaid graph TB @@ -95,7 +18,7 @@ graph TB :core:network[network]:::android-library :core:ui[ui]:::android-library end - subgraph :feature + subgraph :feature direction TB :feature:topic[topic]:::android-feature :feature:foryou[foryou]:::android-feature @@ -142,30 +65,29 @@ visualizing dependencies between modules. The Now in Android app contains the following types of modules: -* The `app` module - contains app level and scaffolding classes that bind the rest of the codebase, - such as `MainActivity`, `NiaApp` and app-level controlled navigation. A good example of this is - the navigation setup through `NiaNavHost` and the bottom navigation bar setup - through `TopLevelDestination`. The `app` module depends on all `feature` modules and - required `core` modules. +### The `app` module +This contains app level and scaffolding classes that bind the rest of the codebase, such as +`MainActivity`, `NiaApp` and app-level controlled navigation. A good example of this is the navigation setup through `NiaNavHost` and the bottom navigation bar setup through `TopLevelDestination`. The `app` module depends on all `feature` modules and required `core` modules. -* `feature:` modules - feature specific modules which are scoped to handle a single responsibility - in the app. These modules can be reused by any app, including test or other flavoured apps, when - needed, while still keeping it separated and isolated. If a class is needed only by one `feature` - module, it should remain within that module. If not, it should be extracted into an - appropriate `core` module. A `feature` module should have no dependencies on other feature - modules. They only depend on the `core` modules that they require. +### Feature modules +These are feature-specific modules that handle a single responsibility in the app. For example, the `ForYou` feature handles all content and UI state for the "ForYou" screen. Feature modules aren't Gradle modules themselves, they are split into two submodules: -* `core:` modules - common library modules containing auxiliary code and specific dependencies that - need to be shared between other modules in the app. These modules can depend on other core - modules, but they shouldn’t depend on feature nor app modules. +* `api` - contains navigation keys +* `impl` - contains everything else -* Miscellaneous modules - such as `sync`, `benchmark` and `test` modules, as well - as `app-nia-catalog` - a catalog app for displaying our design system quickly. +This approach allows features to navigate to other features by using the target feature's navigation keys. A feature's `api` and `impl` modules can be used by any app, including test or other flavoured apps. If a class is needed only by one feature module, it should remain within that module. If not, it should be placed into an appropriate `core` module. +A feature's `api` module should not depend on another feature's `api` or `impl` module. A feature's `impl` should only depend on another feature's `api` module. Both submodules should only depend on the `core` modules that they require. -## Modules +### Core modules +These are common library modules containing auxiliary code and specific dependencies that + need to be shared between other modules in the app. These modules can depend on other core + modules, but they shouldn’t depend on feature nor app modules. + +### Miscellaneous modules +For example, `sync`, `benchmark` and `test` modules, as well as `app-nia-catalog` - a catalog app for displaying our design system quickly. -Using the above modularization strategy, the Now in Android app has the following modules: +## Examples @@ -186,15 +108,28 @@ Using the above modularization strategy, the Now in Android app has the followin - + + + + +
feature:1,
- feature:2
+
feature:1:api,
+ feature:2:api
+ ... +
Navigation keys and functions that other features can use to navigate to this feature.

+ For example: The :topic:api module exposes a Navigator.navigateToTopic function that the + :interests:impl module uses to navigate from the InterestsScreen to the TopicScreen when + a topic is clicked. +
TopicNavKey +
feature:1:impl,
+ feature:2:impl
...
Functionality associated with a specific feature or user journey. Typically contains UI components and ViewModels which read data from other modules.
Examples include:
    -
  • feature:topic displays information about a topic on the TopicScreen.
  • -
  • feature:foryou which displays the user's news feed, and onboarding during first run, on the For You screen.
  • +
  • feature:topic:impl displays information about a topic on the TopicScreen.
  • +
  • feature:foryou:impl which displays the user's news feed, and onboarding during first run, on the For You screen.
TopicScreen
@@ -283,11 +218,12 @@ Using the above modularization strategy, the Now in Android app has the followin
+## Dependency graphs Each module has its own `README.md` file containing a module graph (e.g. [`:app` module graph](../app/README.md#module-dependency-graph)). When modules dependencies change, module graphs are automatically updated by the [Build.yaml](../.github/workflows/Build.yaml) workflow. You can also manually update the graphs by running the `graphUpdate` task. -## Modularization in Now in Android +## Further considerations Our modularization approach was defined taking into account the “Now in Android” project roadmap, upcoming work and new features. Additionally, our aim this time around was to find the right balance between overmodularizing a relatively small app and using this opportunity to showcase a modularization pattern fit for a much larger codebase, closer to real world apps in production environments. diff --git a/feature/foryou/.gitignore b/feature/bookmarks/api/.gitignore similarity index 100% rename from feature/foryou/.gitignore rename to feature/bookmarks/api/.gitignore diff --git a/feature/settings/README.md b/feature/bookmarks/api/README.md similarity index 53% rename from feature/settings/README.md rename to feature/bookmarks/api/README.md index 26c65eba0..7a97f09be 100644 --- a/feature/settings/README.md +++ b/feature/bookmarks/api/README.md @@ -1,4 +1,4 @@ -# `:feature:settings` +# `:feature:bookmarks:api` ## Module dependency graph @@ -11,45 +11,19 @@ config: nodePlacementStrategy: SIMPLE --- graph TB - subgraph :core + subgraph :feature direction TB - :core:analytics[analytics]:::android-library - :core:common[common]:::jvm-library - :core:data[data]:::android-library - :core:database[database]:::android-library - :core:datastore[datastore]:::android-library - :core:datastore-proto[datastore-proto]:::android-library - :core:designsystem[designsystem]:::android-library - :core:model[model]:::jvm-library - :core:network[network]:::android-library - :core:notifications[notifications]:::android-library - :core:ui[ui]:::android-library + subgraph :feature:bookmarks + direction TB + :feature:bookmarks:api[api]:::android-library + end end - subgraph :feature + subgraph :core direction TB - :feature:settings[settings]:::android-feature + :core:navigation[navigation]:::android-library end - :core:data -.-> :core:analytics - :core:data --> :core:common - :core:data --> :core:database - :core:data --> :core:datastore - :core:data --> :core:network - :core:data -.-> :core:notifications - :core:database --> :core:model - :core:datastore -.-> :core:common - :core:datastore --> :core:datastore-proto - :core:datastore --> :core:model - :core:network --> :core:common - :core:network --> :core:model - :core:notifications -.-> :core:common - :core:notifications --> :core:model - :core:ui --> :core:analytics - :core:ui --> :core:designsystem - :core:ui --> :core:model - :feature:settings -.-> :core:data - :feature:settings -.-> :core:designsystem - :feature:settings -.-> :core:ui + :feature:bookmarks:api --> :core:navigation classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/bookmarks/api/build.gradle.kts b/feature/bookmarks/api/build.gradle.kts new file mode 100644 index 000000000..a51468615 --- /dev/null +++ b/feature/bookmarks/api/build.gradle.kts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 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. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.feature.api) +} + +android { + namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks.api" +} \ No newline at end of file diff --git a/feature/bookmarks/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/api/navigation/BookmarksNavKey.kt b/feature/bookmarks/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/api/navigation/BookmarksNavKey.kt new file mode 100644 index 000000000..988266ffc --- /dev/null +++ b/feature/bookmarks/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/api/navigation/BookmarksNavKey.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 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.feature.bookmarks.api.navigation + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +object BookmarksNavKey : NavKey diff --git a/feature/bookmarks/api/src/main/res/drawable/feature_bookmarks_api_mg_empty_bookmarks.xml b/feature/bookmarks/api/src/main/res/drawable/feature_bookmarks_api_mg_empty_bookmarks.xml new file mode 100644 index 000000000..bc12d4325 --- /dev/null +++ b/feature/bookmarks/api/src/main/res/drawable/feature_bookmarks_api_mg_empty_bookmarks.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/feature/bookmarks/src/main/res/values/strings.xml b/feature/bookmarks/api/src/main/res/values/strings.xml similarity index 59% rename from feature/bookmarks/src/main/res/values/strings.xml rename to feature/bookmarks/api/src/main/res/values/strings.xml index 6e2b23043..98f4b4a8d 100644 --- a/feature/bookmarks/src/main/res/values/strings.xml +++ b/feature/bookmarks/api/src/main/res/values/strings.xml @@ -15,10 +15,10 @@ limitations under the License. --> - Saved - Loading saved… - No saved updates - Updates you save will be stored here\nto read later - Bookmark removed - UNDO + Saved + Loading saved… + No saved updates + Updates you save will be stored here\nto read later + Bookmark removed + UNDO diff --git a/feature/interests/.gitignore b/feature/bookmarks/impl/.gitignore similarity index 100% rename from feature/interests/.gitignore rename to feature/bookmarks/impl/.gitignore diff --git a/feature/interests/README.md b/feature/bookmarks/impl/README.md similarity index 77% rename from feature/interests/README.md rename to feature/bookmarks/impl/README.md index 529f8abe7..123a842f6 100644 --- a/feature/interests/README.md +++ b/feature/bookmarks/impl/README.md @@ -1,4 +1,4 @@ -# `:feature:interests` +# `:feature:bookmarks:impl` ## Module dependency graph @@ -11,6 +11,18 @@ config: nodePlacementStrategy: SIMPLE --- graph TB + subgraph :feature + direction TB + subgraph :feature:bookmarks + direction TB + :feature:bookmarks:api[api]:::android-library + :feature:bookmarks:impl[impl]:::android-library + end + subgraph :feature:topic + direction TB + :feature:topic:api[api]:::android-library + end + end subgraph :core direction TB :core:analytics[analytics]:::android-library @@ -20,16 +32,12 @@ graph TB :core:datastore[datastore]:::android-library :core:datastore-proto[datastore-proto]:::android-library :core:designsystem[designsystem]:::android-library - :core:domain[domain]:::android-library :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library :core:network[network]:::android-library :core:notifications[notifications]:::android-library :core:ui[ui]:::android-library end - subgraph :feature - direction TB - :feature:interests[interests]:::android-feature - end :core:data -.-> :core:analytics :core:data --> :core:common @@ -41,8 +49,6 @@ graph TB :core:datastore -.-> :core:common :core:datastore --> :core:datastore-proto :core:datastore --> :core:model - :core:domain --> :core:data - :core:domain --> :core:model :core:network --> :core:common :core:network --> :core:model :core:notifications -.-> :core:common @@ -50,10 +56,15 @@ graph TB :core:ui --> :core:analytics :core:ui --> :core:designsystem :core:ui --> :core:model - :feature:interests -.-> :core:data - :feature:interests -.-> :core:designsystem - :feature:interests -.-> :core:domain - :feature:interests -.-> :core:ui + :feature:bookmarks:api --> :core:navigation + :feature:bookmarks:impl -.-> :core:data + :feature:bookmarks:impl -.-> :core:designsystem + :feature:bookmarks:impl -.-> :core:ui + :feature:bookmarks:impl -.-> :feature:bookmarks:api + :feature:bookmarks:impl -.-> :feature:topic:api + :feature:topic:api -.-> :core:designsystem + :feature:topic:api --> :core:navigation + :feature:topic:api -.-> :core:ui classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/bookmarks/build.gradle.kts b/feature/bookmarks/impl/build.gradle.kts similarity index 85% rename from feature/bookmarks/build.gradle.kts rename to feature/bookmarks/impl/build.gradle.kts index 51a15ce7a..e8162afff 100644 --- a/feature/bookmarks/build.gradle.kts +++ b/feature/bookmarks/impl/build.gradle.kts @@ -15,17 +15,18 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) + alias(libs.plugins.nowinandroid.android.feature.impl) alias(libs.plugins.nowinandroid.android.library.compose) - alias(libs.plugins.nowinandroid.android.library.jacoco) } android { - namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks" + namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks.impl" } dependencies { implementation(projects.core.data) + implementation(projects.feature.bookmarks.api) + implementation(projects.feature.topic.api) testImplementation(projects.core.testing) diff --git a/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreenTest.kt similarity index 96% rename from feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt rename to feature/bookmarks/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreenTest.kt index dd01b84c9..0b73a7918 100644 --- a/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt +++ b/feature/bookmarks/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreenTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.bookmarks +package com.google.samples.apps.nowinandroid.feature.bookmarks.impl import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider @@ -36,6 +36,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.testing.TestLifecycleOwner import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -64,7 +65,7 @@ class BookmarksScreenTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(R.string.feature_bookmarks_loading), + composeTestRule.activity.resources.getString(R.string.feature_bookmarks_api_loading), ) .assertExists() } @@ -161,13 +162,13 @@ class BookmarksScreenTest { composeTestRule .onNodeWithText( - composeTestRule.activity.getString(R.string.feature_bookmarks_empty_error), + composeTestRule.activity.getString(R.string.feature_bookmarks_api_empty_error), ) .assertExists() composeTestRule .onNodeWithText( - composeTestRule.activity.getString(R.string.feature_bookmarks_empty_description), + composeTestRule.activity.getString(R.string.feature_bookmarks_api_empty_description), ) .assertExists() } diff --git a/feature/bookmarks/src/main/AndroidManifest.xml b/feature/bookmarks/impl/src/main/AndroidManifest.xml similarity index 100% rename from feature/bookmarks/src/main/AndroidManifest.xml rename to feature/bookmarks/impl/src/main/AndroidManifest.xml diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreen.kt similarity index 96% rename from feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt rename to feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreen.kt index 7c229c5ea..65bc4acf2 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.bookmarks +package com.google.samples.apps.nowinandroid.feature.bookmarks.impl import androidx.annotation.VisibleForTesting import androidx.compose.foundation.Image @@ -56,7 +56,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -74,9 +74,10 @@ import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider import com.google.samples.apps.nowinandroid.core.ui.newsFeed +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R @Composable -internal fun BookmarksRoute( +internal fun BookmarksScreen( onTopicClick: (String) -> Unit, onShowSnackbar: suspend (String, String?) -> Boolean, modifier: Modifier = Modifier, @@ -112,8 +113,8 @@ internal fun BookmarksScreen( undoBookmarkRemoval: () -> Unit = {}, clearUndoState: () -> Unit = {}, ) { - val bookmarkRemovedMessage = stringResource(id = R.string.feature_bookmarks_removed) - val undoText = stringResource(id = R.string.feature_bookmarks_undo) + val bookmarkRemovedMessage = stringResource(id = R.string.feature_bookmarks_api_removed) + val undoText = stringResource(id = R.string.feature_bookmarks_api_undo) LaunchedEffect(shouldDisplayUndoBookmark) { if (shouldDisplayUndoBookmark) { @@ -155,7 +156,7 @@ private fun LoadingState(modifier: Modifier = Modifier) { .fillMaxWidth() .wrapContentSize() .testTag("forYou:loading"), - contentDesc = stringResource(id = R.string.feature_bookmarks_loading), + contentDesc = stringResource(id = R.string.feature_bookmarks_api_loading), ) } @@ -228,7 +229,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { val iconTint = LocalTintTheme.current.iconTint Image( modifier = Modifier.fillMaxWidth(), - painter = painterResource(id = R.drawable.feature_bookmarks_img_empty_bookmarks), + painter = painterResource(id = R.drawable.feature_bookmarks_api_mg_empty_bookmarks), colorFilter = if (iconTint != Color.Unspecified) ColorFilter.tint(iconTint) else null, contentDescription = null, ) @@ -236,7 +237,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.height(48.dp)) Text( - text = stringResource(id = R.string.feature_bookmarks_empty_error), + text = stringResource(id = R.string.feature_bookmarks_api_empty_error), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleMedium, @@ -246,7 +247,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(id = R.string.feature_bookmarks_empty_description), + text = stringResource(id = R.string.feature_bookmarks_api_empty_description), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium, diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModel.kt similarity index 97% rename from feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt rename to feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModel.kt index f93602485..f36c9d31f 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.bookmarks +package com.google.samples.apps.nowinandroid.feature.bookmarks.impl import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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 new file mode 100644 index 000000000..467965651 --- /dev/null +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt @@ -0,0 +1,49 @@ +/* + * 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.feature.bookmarks.impl.navigation + +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.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.BookmarksScreen +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic + +fun EntryProviderScope.bookmarksEntry(navigator: Navigator) { + entry { + val snackbarHostState = LocalSnackbarHostState.current + BookmarksScreen( + onTopicClick = navigator::navigateToTopic, + onShowSnackbar = { message, action -> + snackbarHostState.showSnackbar( + message = message, + actionLabel = action, + duration = Short, + ) == ActionPerformed + }, + ) + } +} + +// TODO: Why is this here? +val LocalSnackbarHostState = compositionLocalOf { + error("SnackbarHostState state should be initialized at runtime") +} diff --git a/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt b/feature/bookmarks/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModelTest.kt similarity index 97% rename from feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt rename to feature/bookmarks/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModelTest.kt index aa42adae2..66ce0744f 100644 --- a/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt +++ b/feature/bookmarks/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.bookmarks +package com.google.samples.apps.nowinandroid.feature.bookmarks.impl import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData @@ -23,6 +23,7 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.BookmarksViewModel import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt deleted file mode 100644 index ea8d525ab..000000000 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2022 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.feature.bookmarks.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute -import kotlinx.serialization.Serializable - -@Serializable object BookmarksRoute - -fun NavController.navigateToBookmarks(navOptions: NavOptions) = - navigate(route = BookmarksRoute, navOptions) - -fun NavGraphBuilder.bookmarksScreen( - onTopicClick: (String) -> Unit, - onShowSnackbar: suspend (String, String?) -> Boolean, -) { - composable { - BookmarksRoute(onTopicClick, onShowSnackbar) - } -} diff --git a/feature/bookmarks/src/main/res/drawable/feature_bookmarks_img_empty_bookmarks.xml b/feature/bookmarks/src/main/res/drawable/feature_bookmarks_img_empty_bookmarks.xml deleted file mode 100644 index 64bbfbd23..000000000 --- a/feature/bookmarks/src/main/res/drawable/feature_bookmarks_img_empty_bookmarks.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/feature/search/.gitignore b/feature/foryou/api/.gitignore similarity index 100% rename from feature/search/.gitignore rename to feature/foryou/api/.gitignore diff --git a/feature/foryou/api/README.md b/feature/foryou/api/README.md new file mode 100644 index 000000000..81223ecac --- /dev/null +++ b/feature/foryou/api/README.md @@ -0,0 +1,57 @@ +# `:feature:foryou:api` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:foryou + direction TB + :feature:foryou:api[api]:::android-library + end + end + subgraph :core + direction TB + :core:navigation[navigation]:::android-library + end + + :feature:foryou:api --> :core:navigation + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
đź“‹ Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/foryou/api/build.gradle.kts b/feature/foryou/api/build.gradle.kts new file mode 100644 index 000000000..99ac324a2 --- /dev/null +++ b/feature/foryou/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright 2022 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. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.feature.api) +} + +android { + namespace = "com.google.samples.apps.nowinandroid.feature.foryou.api" +} + +dependencies { + api(projects.core.navigation) +} diff --git a/feature/foryou/src/main/AndroidManifest.xml b/feature/foryou/api/src/main/AndroidManifest.xml similarity index 100% rename from feature/foryou/src/main/AndroidManifest.xml rename to feature/foryou/api/src/main/AndroidManifest.xml diff --git a/feature/foryou/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/api/navigation/ForYouNavKey.kt b/feature/foryou/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/api/navigation/ForYouNavKey.kt new file mode 100644 index 000000000..d61ae5a20 --- /dev/null +++ b/feature/foryou/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/api/navigation/ForYouNavKey.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 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.feature.foryou.api.navigation + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +object ForYouNavKey : NavKey diff --git a/feature/foryou/src/main/res/drawable/feature_foryou_ic_icon_placeholder.xml b/feature/foryou/api/src/main/res/drawable/feature_foryou_api_ic_icon_placeholder.xml similarity index 100% rename from feature/foryou/src/main/res/drawable/feature_foryou_ic_icon_placeholder.xml rename to feature/foryou/api/src/main/res/drawable/feature_foryou_api_ic_icon_placeholder.xml diff --git a/feature/foryou/src/main/res/values/strings.xml b/feature/foryou/api/src/main/res/values/strings.xml similarity index 56% rename from feature/foryou/src/main/res/values/strings.xml rename to feature/foryou/api/src/main/res/values/strings.xml index 166749664..f0595944f 100644 --- a/feature/foryou/src/main/res/values/strings.xml +++ b/feature/foryou/api/src/main/res/values/strings.xml @@ -15,11 +15,10 @@ limitations under the License. --> - For you - Done - Loading for you… - Navigate up - What are you interested in? - Updates from topics you follow will appear here. Follow some things to get started. - - + For you + Done + Loading for you… + Navigate up + What are you interested in? + Updates from topics you follow will appear here. Follow some things to get started. + \ No newline at end of file diff --git a/feature/settings/.gitignore b/feature/foryou/impl/.gitignore similarity index 100% rename from feature/settings/.gitignore rename to feature/foryou/impl/.gitignore diff --git a/feature/foryou/README.md b/feature/foryou/impl/README.md similarity index 77% rename from feature/foryou/README.md rename to feature/foryou/impl/README.md index 7cd42863d..8c12460f4 100644 --- a/feature/foryou/README.md +++ b/feature/foryou/impl/README.md @@ -1,4 +1,4 @@ -# `:feature:foryou` +# `:feature:foryou:impl` ## Module dependency graph @@ -11,6 +11,18 @@ config: nodePlacementStrategy: SIMPLE --- graph TB + subgraph :feature + direction TB + subgraph :feature:foryou + direction TB + :feature:foryou:api[api]:::android-library + :feature:foryou:impl[impl]:::android-library + end + subgraph :feature:topic + direction TB + :feature:topic:api[api]:::android-library + end + end subgraph :core direction TB :core:analytics[analytics]:::android-library @@ -22,14 +34,11 @@ graph TB :core:designsystem[designsystem]:::android-library :core:domain[domain]:::android-library :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library :core:network[network]:::android-library :core:notifications[notifications]:::android-library :core:ui[ui]:::android-library end - subgraph :feature - direction TB - :feature:foryou[foryou]:::android-feature - end :core:data -.-> :core:analytics :core:data --> :core:common @@ -50,11 +59,16 @@ graph TB :core:ui --> :core:analytics :core:ui --> :core:designsystem :core:ui --> :core:model - :feature:foryou -.-> :core:data - :feature:foryou -.-> :core:designsystem - :feature:foryou -.-> :core:domain - :feature:foryou -.-> :core:notifications - :feature:foryou -.-> :core:ui + :feature:foryou:api --> :core:navigation + :feature:foryou:impl -.-> :core:designsystem + :feature:foryou:impl -.-> :core:domain + :feature:foryou:impl -.-> :core:notifications + :feature:foryou:impl -.-> :core:ui + :feature:foryou:impl -.-> :feature:foryou:api + :feature:foryou:impl -.-> :feature:topic:api + :feature:topic:api -.-> :core:designsystem + :feature:topic:api --> :core:navigation + :feature:topic:api -.-> :core:ui classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/foryou/build.gradle.kts b/feature/foryou/impl/build.gradle.kts similarity index 86% rename from feature/foryou/build.gradle.kts rename to feature/foryou/impl/build.gradle.kts index de1af7540..7964f8c4f 100644 --- a/feature/foryou/build.gradle.kts +++ b/feature/foryou/impl/build.gradle.kts @@ -15,22 +15,23 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) + alias(libs.plugins.nowinandroid.android.feature.impl) alias(libs.plugins.nowinandroid.android.library.compose) - alias(libs.plugins.nowinandroid.android.library.jacoco) alias(libs.plugins.roborazzi) } android { - namespace = "com.google.samples.apps.nowinandroid.feature.foryou" + namespace = "com.google.samples.apps.nowinandroid.feature.foryou.impl" testOptions.unitTests.isIncludeAndroidResources = true } dependencies { implementation(libs.accompanist.permissions) - implementation(projects.core.data) implementation(projects.core.domain) implementation(projects.core.notifications) + implementation(projects.feature.foryou.api) + implementation(projects.feature.topic.api) + implementation(libs.androidx.activity.compose) testImplementation(libs.hilt.android.testing) testImplementation(libs.robolectric) @@ -39,4 +40,4 @@ dependencies { androidTestImplementation(libs.bundles.androidx.compose.ui.test) androidTestImplementation(projects.core.testing) -} +} \ No newline at end of file diff --git a/feature/foryou/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt b/feature/foryou/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenTest.kt similarity index 95% rename from feature/foryou/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt rename to feature/foryou/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenTest.kt index c3ec5c560..fc4fc0241 100644 --- a/feature/foryou/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt +++ b/feature/foryou/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Box @@ -32,6 +32,7 @@ import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPer import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState +import com.google.samples.apps.nowinandroid.feature.foryou.api.R import org.junit.Rule import org.junit.Test @@ -45,7 +46,7 @@ class ForYouScreenTest { private val doneButtonMatcher by lazy { hasText( - composeTestRule.activity.resources.getString(R.string.feature_foryou_done), + composeTestRule.activity.resources.getString(R.string.feature_foryou_api_done), ) } @@ -70,7 +71,7 @@ class ForYouScreenTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(R.string.feature_foryou_loading), + composeTestRule.activity.resources.getString(R.string.feature_foryou_api_loading), ) .assertExists() } @@ -96,7 +97,7 @@ class ForYouScreenTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(R.string.feature_foryou_loading), + composeTestRule.activity.resources.getString(R.string.feature_foryou_api_loading), ) .assertExists() } @@ -200,7 +201,9 @@ class ForYouScreenTest { ForYouScreen( isSyncing = false, onboardingUiState = - OnboardingUiState.Shown(topics = followableTopicTestData), + OnboardingUiState.Shown( + topics = followableTopicTestData, + ), feedState = NewsFeedUiState.Loading, deepLinkedUserNewsResource = null, onTopicCheckedChanged = { _, _ -> }, @@ -215,7 +218,7 @@ class ForYouScreenTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(R.string.feature_foryou_loading), + composeTestRule.activity.resources.getString(R.string.feature_foryou_api_loading), ) .assertExists() } @@ -241,7 +244,7 @@ class ForYouScreenTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(R.string.feature_foryou_loading), + composeTestRule.activity.resources.getString(R.string.feature_foryou_api_loading), ) .assertExists() } diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreen.kt similarity index 98% rename from feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt rename to feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreen.kt index 1a3325996..0ae916db3 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import android.net.Uri import android.os.Build.VERSION @@ -80,7 +80,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus.Denied @@ -103,9 +103,10 @@ import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider import com.google.samples.apps.nowinandroid.core.ui.launchCustomChromeTab import com.google.samples.apps.nowinandroid.core.ui.newsFeed +import com.google.samples.apps.nowinandroid.feature.foryou.api.R @Composable -internal fun ForYouScreen( +fun ForYouScreen( onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, viewModel: ForYouViewModel = hiltViewModel(), @@ -215,7 +216,7 @@ internal fun ForYouScreen( targetOffsetY = { fullHeight -> -fullHeight }, ) + fadeOut(), ) { - val loadingContentDescription = stringResource(id = R.string.feature_foryou_loading) + val loadingContentDescription = stringResource(id = R.string.feature_foryou_api_loading) Box( modifier = Modifier .fillMaxWidth() @@ -270,7 +271,7 @@ private fun LazyStaggeredGridScope.onboarding( item(span = StaggeredGridItemSpan.FullLine, contentType = "onboarding") { Column(modifier = interestsItemModifier) { Text( - text = stringResource(R.string.feature_foryou_onboarding_guidance_title), + text = stringResource(R.string.feature_foryou_api_onboarding_guidance_title), textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() @@ -278,7 +279,7 @@ private fun LazyStaggeredGridScope.onboarding( style = MaterialTheme.typography.titleMedium, ) Text( - text = stringResource(R.string.feature_foryou_onboarding_guidance_subtitle), + text = stringResource(R.string.feature_foryou_api_onboarding_guidance_subtitle), modifier = Modifier .fillMaxWidth() .padding(top = 8.dp, start = 24.dp, end = 24.dp), @@ -304,7 +305,7 @@ private fun LazyStaggeredGridScope.onboarding( .fillMaxWidth(), ) { Text( - text = stringResource(R.string.feature_foryou_done), + text = stringResource(R.string.feature_foryou_api_done), ) } } @@ -433,7 +434,7 @@ fun TopicIcon( modifier: Modifier = Modifier, ) { DynamicAsyncImage( - placeholder = painterResource(R.drawable.feature_foryou_ic_icon_placeholder), + placeholder = painterResource(R.drawable.feature_foryou_api_ic_icon_placeholder), imageUrl = imageUrl, // decorative contentDescription = null, diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModel.kt similarity index 98% rename from feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt rename to feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModel.kt index 4b6cd39c9..c54551c0b 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/OnboardingUiState.kt similarity index 95% rename from feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt rename to feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/OnboardingUiState.kt index 70634b4c5..d31749bb5 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/OnboardingUiState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic 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 new file mode 100644 index 000000000..4bdc7368c --- /dev/null +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt @@ -0,0 +1,32 @@ +/* + * 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.feature.foryou.impl.navigation + +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey +import com.google.samples.apps.nowinandroid.feature.foryou.impl.ForYouScreen +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic + +fun EntryProviderScope.forYouEntry(navigator: Navigator) { + entry { + ForYouScreen( + onTopicClick = navigator::navigateToTopic, + ) + } +} diff --git a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenScreenshotTests.kt b/feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenScreenshotTests.kt similarity index 94% rename from feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenScreenshotTests.kt rename to feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenScreenshotTests.kt index 29fc6f536..d0d73860e 100644 --- a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenScreenshotTests.kt +++ b/feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenScreenshotTests.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable @@ -31,9 +31,8 @@ import com.google.samples.apps.nowinandroid.core.testing.util.captureMultiDevice import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider -import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.Loading -import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.NotShown -import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.Shown +import com.google.samples.apps.nowinandroid.feature.foryou.impl.OnboardingUiState.NotShown +import com.google.samples.apps.nowinandroid.feature.foryou.impl.OnboardingUiState.Shown import dagger.hilt.android.testing.HiltTestApplication import org.hamcrest.Matchers import org.junit.Before @@ -97,7 +96,7 @@ class ForYouScreenScreenshotTests { NiaTheme { ForYouScreen( isSyncing = false, - onboardingUiState = Loading, + onboardingUiState = OnboardingUiState.Loading, feedState = NewsFeedUiState.Loading, onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, @@ -194,7 +193,7 @@ class ForYouScreenScreenshotTests { NiaTheme { ForYouScreen( isSyncing = true, - onboardingUiState = Loading, + onboardingUiState = OnboardingUiState.Loading, feedState = Success( feed = userNewsResources, ), diff --git a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModelTest.kt similarity index 99% rename from feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt rename to feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModelTest.kt index 812544c0c..5008b484c 100644 --- a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import androidx.lifecycle.SavedStateHandle import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_foldable.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_foldable.png new file mode 100644 index 000000000..51ffcce75 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_foldable.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_phone.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_phone.png new file mode 100644 index 000000000..e6c3b6c5a Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_phone.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_tablet.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_tablet.png new file mode 100644 index 000000000..eebc53e33 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_tablet.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png new file mode 100644 index 000000000..d7e5af23b Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png new file mode 100644 index 000000000..4bc0969af Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png new file mode 100644 index 000000000..e52802664 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png new file mode 100644 index 000000000..3cecfec33 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png new file mode 100644 index 000000000..cbe01938b Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png new file mode 100644 index 000000000..2ef02d5ce Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png new file mode 100644 index 000000000..849fd488b Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_foldable.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_foldable.png new file mode 100644 index 000000000..6ea161e10 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_foldable.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone.png new file mode 100644 index 000000000..eb7de8009 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone_dark.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone_dark.png new file mode 100644 index 000000000..693db55ff Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone_dark.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_tablet.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_tablet.png new file mode 100644 index 000000000..ffa2b2262 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_tablet.png differ diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt deleted file mode 100644 index b77ce72a0..000000000 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2022 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.feature.foryou.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import androidx.navigation.compose.navigation -import androidx.navigation.navDeepLink -import com.google.samples.apps.nowinandroid.core.notifications.DEEP_LINK_URI_PATTERN -import com.google.samples.apps.nowinandroid.feature.foryou.ForYouScreen -import kotlinx.serialization.Serializable - -@Serializable data object ForYouRoute // route to ForYou screen - -@Serializable data object ForYouBaseRoute // route to base navigation graph - -fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(route = ForYouRoute, navOptions) - -/** - * The ForYou section of the app. It can also display information about topics. - * This should be supplied from a separate module. - * - * @param onTopicClick - Called when a topic is clicked, contains the ID of the topic - * @param topicDestination - Destination for topic content - */ -fun NavGraphBuilder.forYouSection( - onTopicClick: (String) -> Unit, - topicDestination: NavGraphBuilder.() -> Unit, -) { - navigation(startDestination = ForYouRoute) { - composable( - deepLinks = listOf( - navDeepLink { - /** - * This destination has a deep link that enables a specific news resource to be - * opened from a notification (@see SystemTrayNotifier for more). The news resource - * ID is sent in the URI rather than being modelled in the route type because it's - * transient data (stored in SavedStateHandle) that is cleared after the user has - * opened the news resource. - */ - uriPattern = DEEP_LINK_URI_PATTERN - }, - ), - ) { - ForYouScreen(onTopicClick) - } - topicDestination() - } -} diff --git a/feature/foryou/src/test/screenshots/ForYouScreenLoading_foldable.png b/feature/foryou/src/test/screenshots/ForYouScreenLoading_foldable.png deleted file mode 100644 index 538c9032c..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenLoading_foldable.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenLoading_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenLoading_phone.png deleted file mode 100644 index afd7dd708..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenLoading_phone.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenLoading_tablet.png b/feature/foryou/src/test/screenshots/ForYouScreenLoading_tablet.png deleted file mode 100644 index 1c197e8f0..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenLoading_tablet.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png deleted file mode 100644 index a86a8232f..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png deleted file mode 100644 index d500394ef..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png deleted file mode 100644 index 1498f9b7b..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png and /dev/null differ diff --git a/feature/topic/.gitignore b/feature/interests/api/.gitignore similarity index 100% rename from feature/topic/.gitignore rename to feature/interests/api/.gitignore diff --git a/feature/interests/api/README.md b/feature/interests/api/README.md new file mode 100644 index 000000000..ef580df7c --- /dev/null +++ b/feature/interests/api/README.md @@ -0,0 +1,57 @@ +# `:feature:interests:api` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:interests + direction TB + :feature:interests:api[api]:::android-library + end + end + subgraph :core + direction TB + :core:navigation[navigation]:::android-library + end + + :feature:interests:api --> :core:navigation + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
đź“‹ Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/interests/build.gradle.kts b/feature/interests/api/build.gradle.kts similarity index 58% rename from feature/interests/build.gradle.kts rename to feature/interests/api/build.gradle.kts index 2b84b135f..7a2dfd65e 100644 --- a/feature/interests/build.gradle.kts +++ b/feature/interests/api/build.gradle.kts @@ -15,21 +15,9 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) - alias(libs.plugins.nowinandroid.android.library.compose) - alias(libs.plugins.nowinandroid.android.library.jacoco) + alias(libs.plugins.nowinandroid.android.feature.api) } -android { - namespace = "com.google.samples.apps.nowinandroid.feature.interests" -} - -dependencies { - implementation(projects.core.data) - implementation(projects.core.domain) - testImplementation(projects.core.testing) - testImplementation(libs.robolectric) - - androidTestImplementation(libs.bundles.androidx.compose.ui.test) - androidTestImplementation(projects.core.testing) -} +android { + namespace = "com.google.samples.apps.nowinandroid.feature.interests.api" +} \ No newline at end of file diff --git a/feature/interests/src/main/AndroidManifest.xml b/feature/interests/api/src/main/AndroidManifest.xml similarity index 100% rename from feature/interests/src/main/AndroidManifest.xml rename to feature/interests/api/src/main/AndroidManifest.xml diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt b/feature/interests/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/api/navigation/InterestsNavKey.kt similarity index 67% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt rename to feature/interests/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/api/navigation/InterestsNavKey.kt index d83e4a9b2..cd6c631a2 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt +++ b/feature/interests/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/api/navigation/InterestsNavKey.kt @@ -14,20 +14,13 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.interests.navigation +package com.google.samples.apps.nowinandroid.feature.interests.api.navigation -import androidx.navigation.NavController -import androidx.navigation.NavOptions +import androidx.navigation3.runtime.NavKey import kotlinx.serialization.Serializable -@Serializable data class InterestsRoute( +@Serializable +data class InterestsNavKey( // The ID of the topic which will be initially selected at this destination val initialTopicId: String? = null, -) - -fun NavController.navigateToInterests( - initialTopicId: String? = null, - navOptions: NavOptions? = null, -) { - navigate(route = InterestsRoute(initialTopicId), navOptions) -} +) : NavKey diff --git a/feature/topic/src/main/res/drawable/feature_topic_ic_topic_placeholder.xml b/feature/interests/api/src/main/res/drawable/feature_interests_api_ic_detail_placeholder.xml similarity index 99% rename from feature/topic/src/main/res/drawable/feature_topic_ic_topic_placeholder.xml rename to feature/interests/api/src/main/res/drawable/feature_interests_api_ic_detail_placeholder.xml index 0518401da..2789b54e6 100644 --- a/feature/topic/src/main/res/drawable/feature_topic_ic_topic_placeholder.xml +++ b/feature/interests/api/src/main/res/drawable/feature_interests_api_ic_detail_placeholder.xml @@ -52,4 +52,4 @@ android:strokeColor="#8C4190" android:strokeLineCap="round" android:strokeWidth="2" /> - + \ No newline at end of file diff --git a/feature/interests/src/main/res/values/strings.xml b/feature/interests/api/src/main/res/values/strings.xml similarity index 69% rename from feature/interests/src/main/res/values/strings.xml rename to feature/interests/api/src/main/res/values/strings.xml index 8d5322859..b02d91bcc 100644 --- a/feature/interests/src/main/res/values/strings.xml +++ b/feature/interests/api/src/main/res/values/strings.xml @@ -15,7 +15,8 @@ limitations under the License. --> - Interests - Loading data - "No available data" + Interests + Loading data + "No available data" + Select an Interest diff --git a/feature/interests/impl/.gitignore b/feature/interests/impl/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/interests/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/interests/impl/README.md b/feature/interests/impl/README.md new file mode 100644 index 000000000..722f4ef41 --- /dev/null +++ b/feature/interests/impl/README.md @@ -0,0 +1,3 @@ +# :feature:interests:impl module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_feature_interests_impl.svg) diff --git a/feature/interests/impl/build.gradle.kts b/feature/interests/impl/build.gradle.kts new file mode 100644 index 000000000..fedc7fba6 --- /dev/null +++ b/feature/interests/impl/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * Copyright 2022 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. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.feature.impl) + alias(libs.plugins.nowinandroid.android.library.compose) + alias(libs.plugins.nowinandroid.android.library.jacoco) +} +android { + namespace = "com.google.samples.apps.nowinandroid.feature.interests.impl" + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(projects.core.domain) + implementation(projects.feature.topic.api) + implementation(projects.feature.interests.api) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.androidx.compose.material3.adaptive.layout) + implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.navigation3) + + testImplementation(projects.core.testing) + testImplementation(projects.core.dataTest) + testImplementation(libs.robolectric) + testImplementation(libs.androidx.compose.ui.test) + testImplementation(libs.androidx.test.espresso.core) + testImplementation(libs.hilt.android.testing) + testImplementation(projects.uiTestHiltManifest) + testImplementation(projects.feature.topic.impl) + testImplementation(libs.androidx.navigation.testing) + + androidTestImplementation(libs.bundles.androidx.compose.ui.test) + androidTestImplementation(projects.core.testing) +} diff --git a/feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt b/feature/interests/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreenTest.kt similarity index 92% rename from feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt rename to feature/interests/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreenTest.kt index a441f5a9d..8a10a478b 100644 --- a/feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt +++ b/feature/interests/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreenTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.interests +package com.google.samples.apps.nowinandroid.feature.interests.impl import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable @@ -25,13 +25,11 @@ import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData -import com.google.samples.apps.nowinandroid.feature.interests.InterestsScreen -import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState import org.junit.Before import org.junit.Rule import org.junit.Test import com.google.samples.apps.nowinandroid.core.ui.R as CoreUiR -import com.google.samples.apps.nowinandroid.feature.interests.R as InterestsR +import com.google.samples.apps.nowinandroid.feature.interests.api.R as InterestsR /** * UI test for checking the correct behaviour of the Interests screen; @@ -51,8 +49,8 @@ class InterestsScreenTest { @Before fun setup() { composeTestRule.activity.apply { - interestsLoading = getString(InterestsR.string.feature_interests_loading) - interestsEmptyHeader = getString(InterestsR.string.feature_interests_empty_header) + interestsLoading = getString(InterestsR.string.feature_interests_api_loading) + interestsEmptyHeader = getString(InterestsR.string.feature_interests_api_empty_header) interestsTopicCardFollowButton = getString(CoreUiR.string.core_ui_interests_card_follow_button_content_desc) interestsTopicCardUnfollowButton = diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicDetailPlaceholder.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsDetailPlaceholder.kt similarity index 86% rename from feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicDetailPlaceholder.kt rename to feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsDetailPlaceholder.kt index 627fb8fb3..2a03019d3 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicDetailPlaceholder.kt +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsDetailPlaceholder.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.topic +package com.google.samples.apps.nowinandroid.feature.interests.impl import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -33,9 +33,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme +import com.google.samples.apps.nowinandroid.feature.interests.api.R @Composable -fun TopicDetailPlaceholder(modifier: Modifier = Modifier) { +fun InterestsDetailPlaceholder(modifier: Modifier = Modifier) { Card( modifier = modifier, colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), @@ -50,12 +51,12 @@ fun TopicDetailPlaceholder(modifier: Modifier = Modifier) { ), ) { Icon( - painter = painterResource(id = R.drawable.feature_topic_ic_topic_placeholder), + painter = painterResource(id = R.drawable.feature_interests_api_ic_detail_placeholder), contentDescription = null, tint = MaterialTheme.colorScheme.primary, ) Text( - text = stringResource(id = R.string.feature_topic_select_an_interest), + text = stringResource(id = R.string.feature_interests_api_select_an_interest), style = MaterialTheme.typography.titleLarge, ) } @@ -66,6 +67,6 @@ fun TopicDetailPlaceholder(modifier: Modifier = Modifier) { @Composable fun TopicDetailPlaceholderPreview() { NiaTheme { - TopicDetailPlaceholder() + InterestsDetailPlaceholder() } } diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreen.kt similarity index 91% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt rename to feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreen.kt index 9b18ac89b..689b5bbf0 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.interests +package com.google.samples.apps.nowinandroid.feature.interests.impl import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text @@ -24,7 +24,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel @@ -33,13 +32,14 @@ import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.FollowableTopicPreviewParameterProvider import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent +import com.google.samples.apps.nowinandroid.feature.interests.api.R @Composable -fun InterestsRoute( +fun InterestsScreen( onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, + viewModel: InterestsViewModel, shouldHighlightSelectedTopic: Boolean = false, - viewModel: InterestsViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -47,6 +47,7 @@ fun InterestsRoute( uiState = uiState, followTopic = viewModel::followTopic, onTopicClick = { + // TODO: this violates SSOT, events should go through the ViewModel viewModel.onTopicClick(it) onTopicClick(it) }, @@ -70,7 +71,7 @@ internal fun InterestsScreen( when (uiState) { InterestsUiState.Loading -> NiaLoadingWheel( - contentDesc = stringResource(id = R.string.feature_interests_loading), + contentDesc = stringResource(id = R.string.feature_interests_api_loading), ) is InterestsUiState.Interests -> @@ -90,7 +91,7 @@ internal fun InterestsScreen( @Composable private fun InterestsEmptyScreen() { - Text(text = stringResource(id = R.string.feature_interests_empty_header)) + Text(text = stringResource(id = R.string.feature_interests_api_empty_header)) } @DevicePreviews diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsViewModel.kt similarity index 74% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt rename to feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsViewModel.kt index 67cc8884f..f79d79d09 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsViewModel.kt @@ -14,39 +14,43 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.interests +package com.google.samples.apps.nowinandroid.feature.interests.impl import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.navigation.toRoute import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.domain.TopicSortField import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic -import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class InterestsViewModel @Inject constructor( +@HiltViewModel(assistedFactory = InterestsViewModel.Factory::class) +class InterestsViewModel @AssistedInject constructor( private val savedStateHandle: SavedStateHandle, val userDataRepository: UserDataRepository, getFollowableTopics: GetFollowableTopicsUseCase, + // TODO: see comment below + @Assisted val key: InterestsNavKey, ) : ViewModel() { + // TODO: this should no longer be necessary, the currently selected topic should be + // available through the navigation state // Key used to save and retrieve the currently selected topic id from saved state. private val selectedTopicIdKey = "selectedTopicIdKey" - private val interestsRoute: InterestsRoute = savedStateHandle.toRoute() private val selectedTopicId = savedStateHandle.getStateFlow( key = selectedTopicIdKey, - initialValue = interestsRoute.initialTopicId, + initialValue = key.initialTopicId, ) val uiState: StateFlow = combine( @@ -66,8 +70,15 @@ class InterestsViewModel @Inject constructor( } fun onTopicClick(topicId: String?) { + // TODO: This should modify the navigation state directly rather than just updating the + // savedStateHandle savedStateHandle[selectedTopicIdKey] = topicId } + + @AssistedFactory + interface Factory { + fun create(key: InterestsNavKey): InterestsViewModel + } } sealed interface InterestsUiState { diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/TabContent.kt similarity index 96% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt rename to feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/TabContent.kt index 133c2bedd..d8a09c8f0 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/TabContent.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.interests +package com.google.samples.apps.nowinandroid.feature.interests.impl import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box @@ -59,7 +59,7 @@ fun TopicsTabContent( LazyColumn( modifier = Modifier .padding(horizontal = 24.dp) - .testTag("interests:topics"), + .testTag(LIST_PANE_TEST_TAG), contentPadding = PaddingValues(vertical = 16.dp), state = scrollableState, ) { @@ -103,3 +103,5 @@ fun TopicsTabContent( ) } } + +val LIST_PANE_TEST_TAG = "interests:topics" 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 new file mode 100644 index 000000000..b92f462eb --- /dev/null +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt @@ -0,0 +1,51 @@ +/* + * 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.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.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsDetailPlaceholder +import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsScreen +import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsViewModel +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +fun EntryProviderScope.interestsEntry(navigator: Navigator) { + entry( + metadata = ListDetailSceneStrategy.listPane { + InterestsDetailPlaceholder() + }, + ) { key -> + val viewModel = hiltViewModel { + it.create(key) + } + InterestsScreen( + // TODO: This event should either be provided by the ViewModel or by the navigator, not both + onTopicClick = navigator::navigateToTopic, + + // TODO: This should be dynamically calculated based on the rendering scene + // See https://github.com/android/nav3-recipes/commit/488f4811791ca3ed7192f4fe3c86e7371b32ebdc#diff-374e02026cdd2f68057dd940f203dc4ba7319930b33e9555c61af7e072211cabR89 + shouldHighlightSelectedTopic = false, + viewModel = viewModel, + ) + } +} diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/InterestsListDetailScreenTest.kt b/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsListDetailScreenTest.kt similarity index 68% rename from app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/InterestsListDetailScreenTest.kt rename to feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsListDetailScreenTest.kt index 1062c7e56..0975216d1 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/InterestsListDetailScreenTest.kt +++ b/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsListDetailScreenTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Android Open Source Project + * 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. @@ -14,10 +14,14 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.ui +@file:OptIn(ExperimentalMaterial3AdaptiveApi::class) + +package com.google.samples.apps.nowinandroid.interests.impl -import androidx.activity.compose.BackHandler import androidx.annotation.StringRes +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy +import androidx.compose.runtime.Composable import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.junit4.AndroidComposeTestRule @@ -25,11 +29,20 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +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.ui.interests2pane.InterestsListDetailScreen +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.core.navigation.rememberNavigationState +import com.google.samples.apps.nowinandroid.core.navigation.toEntries +import com.google.samples.apps.nowinandroid.feature.interests.api.R +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import com.google.samples.apps.nowinandroid.feature.interests.impl.LIST_PANE_TEST_TAG +import com.google.samples.apps.nowinandroid.feature.interests.impl.navigation.interestsEntry +import com.google.samples.apps.nowinandroid.feature.topic.impl.navigation.topicEntry import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -43,16 +56,15 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import javax.inject.Inject +import kotlin.getValue import kotlin.properties.ReadOnlyProperty -import kotlin.test.assertTrue -import com.google.samples.apps.nowinandroid.feature.topic.R as FeatureTopicR private const val EXPANDED_WIDTH = "w1200dp-h840dp" private const val COMPACT_WIDTH = "w412dp-h915dp" @HiltAndroidTest @RunWith(RobolectricTestRunner::class) -@Config(application = HiltTestApplication::class) +@Config(application = HiltTestApplication::class, sdk = [35]) class InterestsListDetailScreenTest { @get:Rule(order = 0) @@ -70,8 +82,7 @@ class InterestsListDetailScreenTest { } // The strings used for matching in these tests. - private val placeholderText by composeTestRule.stringResource(FeatureTopicR.string.feature_topic_select_an_interest) - private val listPaneTag = "interests:topics" + private val placeholderText by composeTestRule.stringResource(R.string.feature_interests_api_select_an_interest) private val Topic.testTag get() = "topic:${this.id}" @@ -87,26 +98,48 @@ class InterestsListDetailScreenTest { composeTestRule.apply { setContent { NiaTheme { - InterestsListDetailScreen() + TestNavDisplay() } } - - onNodeWithTag(listPaneTag).assertIsDisplayed() + onNodeWithTag(LIST_PANE_TEST_TAG).assertIsDisplayed() onNodeWithText(placeholderText).assertIsDisplayed() } } + @Composable + private fun TestNavDisplay() { + val startKey = InterestsNavKey(null) + + val navigationState = rememberNavigationState( + startKey = startKey, + topLevelKeys = setOf(startKey), + ) + + val navigator = Navigator(navigationState) + + val entryProvider = entryProvider { + interestsEntry(navigator) + topicEntry(navigator) + } + + NavDisplay( + entries = navigationState.toEntries(entryProvider), + onBack = { navigator.goBack() }, + sceneStrategy = rememberListDetailSceneStrategy(), + ) + } + @Test @Config(qualifiers = COMPACT_WIDTH) fun compactWidth_initialState_showsListPane() { composeTestRule.apply { setContent { NiaTheme { - InterestsListDetailScreen() + TestNavDisplay() } } - onNodeWithTag(listPaneTag).assertIsDisplayed() + onNodeWithTag(LIST_PANE_TEST_TAG).assertIsDisplayed() onNodeWithText(placeholderText).assertIsNotDisplayed() } } @@ -117,14 +150,14 @@ class InterestsListDetailScreenTest { composeTestRule.apply { setContent { NiaTheme { - InterestsListDetailScreen() + TestNavDisplay() } } - val firstTopic = getTopics().first() onNodeWithText(firstTopic.name).performClick() + waitForIdle() - onNodeWithTag(listPaneTag).assertIsDisplayed() + onNodeWithTag(LIST_PANE_TEST_TAG).assertIsDisplayed() onNodeWithText(placeholderText).assertIsNotDisplayed() onNodeWithTag(firstTopic.testTag).assertIsDisplayed() } @@ -136,52 +169,26 @@ class InterestsListDetailScreenTest { composeTestRule.apply { setContent { NiaTheme { - InterestsListDetailScreen() + TestNavDisplay() } } val firstTopic = getTopics().first() onNodeWithText(firstTopic.name).performClick() - onNodeWithTag(listPaneTag).assertIsNotDisplayed() + onNodeWithTag(LIST_PANE_TEST_TAG).assertIsNotDisplayed() onNodeWithText(placeholderText).assertIsNotDisplayed() onNodeWithTag(firstTopic.testTag).assertIsDisplayed() } } - @Test - @Config(qualifiers = EXPANDED_WIDTH) - fun expandedWidth_backPressFromTopicDetail_leavesInterests() { - var unhandledBackPress = false - composeTestRule.apply { - setContent { - NiaTheme { - // Back press should not be handled by the two pane layout, and thus - // "fall through" to this BackHandler. - BackHandler { - unhandledBackPress = true - } - InterestsListDetailScreen() - } - } - - val firstTopic = getTopics().first() - onNodeWithText(firstTopic.name).performClick() - - waitForIdle() - Espresso.pressBack() - - assertTrue(unhandledBackPress) - } - } - @Test @Config(qualifiers = COMPACT_WIDTH) fun compactWidth_backPressFromTopicDetail_showsListPane() { composeTestRule.apply { setContent { NiaTheme { - InterestsListDetailScreen() + TestNavDisplay() } } @@ -191,7 +198,7 @@ class InterestsListDetailScreenTest { waitForIdle() Espresso.pressBack() - onNodeWithTag(listPaneTag).assertIsDisplayed() + onNodeWithTag(LIST_PANE_TEST_TAG).assertIsDisplayed() onNodeWithText(placeholderText).assertIsNotDisplayed() onNodeWithTag(firstTopic.testTag).assertIsNotDisplayed() } diff --git a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt b/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsViewModelTest.kt similarity index 93% rename from feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt rename to feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsViewModelTest.kt index cdf21f325..4e964b52b 100644 --- a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt +++ b/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.interests +package com.google.samples.apps.nowinandroid.interests.impl import androidx.lifecycle.SavedStateHandle import androidx.navigation.testing.invoke @@ -24,9 +24,9 @@ import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule -import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState -import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel -import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsUiState +import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsViewModel import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -36,6 +36,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import kotlin.test.assertEquals /** @@ -49,6 +50,7 @@ import kotlin.test.assertEquals * See https://issuetracker.google.com/340966212. */ @RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) class InterestsViewModelTest { @get:Rule @@ -66,10 +68,11 @@ class InterestsViewModelTest { fun setup() { viewModel = InterestsViewModel( savedStateHandle = SavedStateHandle( - route = InterestsRoute(initialTopicId = testInputTopics[0].topic.id), + route = InterestsNavKey(initialTopicId = testInputTopics[0].topic.id), ), userDataRepository = userDataRepository, getFollowableTopics = getFollowableTopicsUseCase, + InterestsNavKey(initialTopicId = testInputTopics[0].topic.id), ) } diff --git a/feature/search/api/.gitignore b/feature/search/api/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/search/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/topic/README.md b/feature/search/api/README.md similarity index 86% rename from feature/topic/README.md rename to feature/search/api/README.md index 8f88be578..a468f649d 100644 --- a/feature/topic/README.md +++ b/feature/search/api/README.md @@ -1,4 +1,4 @@ -# `:feature:topic` +# `:feature:search:api` ## Module dependency graph @@ -11,6 +11,13 @@ config: nodePlacementStrategy: SIMPLE --- graph TB + subgraph :feature + direction TB + subgraph :feature:search + direction TB + :feature:search:api[api]:::android-library + end + end subgraph :core direction TB :core:analytics[analytics]:::android-library @@ -19,15 +26,11 @@ graph TB :core:database[database]:::android-library :core:datastore[datastore]:::android-library :core:datastore-proto[datastore-proto]:::android-library - :core:designsystem[designsystem]:::android-library + :core:domain[domain]:::android-library :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library :core:network[network]:::android-library :core:notifications[notifications]:::android-library - :core:ui[ui]:::android-library - end - subgraph :feature - direction TB - :feature:topic[topic]:::android-feature end :core:data -.-> :core:analytics @@ -40,16 +43,14 @@ graph TB :core:datastore -.-> :core:common :core:datastore --> :core:datastore-proto :core:datastore --> :core:model + :core:domain --> :core:data + :core:domain --> :core:model :core:network --> :core:common :core:network --> :core:model :core:notifications -.-> :core:common :core:notifications --> :core:model - :core:ui --> :core:analytics - :core:ui --> :core:designsystem - :core:ui --> :core:model - :feature:topic -.-> :core:data - :feature:topic -.-> :core:designsystem - :feature:topic -.-> :core:ui + :feature:search:api -.-> :core:domain + :feature:search:api --> :core:navigation classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/search/api/build.gradle.kts b/feature/search/api/build.gradle.kts new file mode 100644 index 000000000..d7ea6fc5f --- /dev/null +++ b/feature/search/api/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright 2023 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. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.feature.api) +} + +android { + namespace = "com.google.samples.apps.nowinandroid.feature.search.api" +} + +dependencies { + implementation(projects.core.domain) +} + diff --git a/feature/search/src/main/AndroidManifest.xml b/feature/search/api/src/main/AndroidManifest.xml similarity index 100% rename from feature/search/src/main/AndroidManifest.xml rename to feature/search/api/src/main/AndroidManifest.xml diff --git a/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchNavKey.kt b/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchNavKey.kt new file mode 100644 index 000000000..9588a8404 --- /dev/null +++ b/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchNavKey.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 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.feature.search.api.navigation + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +object SearchNavKey : NavKey diff --git a/feature/search/api/src/main/res/values/strings.xml b/feature/search/api/src/main/res/values/strings.xml new file mode 100644 index 000000000..d2d218493 --- /dev/null +++ b/feature/search/api/src/main/res/values/strings.xml @@ -0,0 +1,29 @@ + + + + Search + Clear search text + Sorry, there is no content found for your search \"%1$s\" + Sorry, we are still processing the search index. Please come back later + Try another search or explorer + Interests + to browse topics + Topics + Updates + Recent searches + Clear searches + diff --git a/feature/search/impl/.gitignore b/feature/search/impl/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/search/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/search/README.md b/feature/search/impl/README.md similarity index 73% rename from feature/search/README.md rename to feature/search/impl/README.md index d8160a627..04800005c 100644 --- a/feature/search/README.md +++ b/feature/search/impl/README.md @@ -1,4 +1,4 @@ -# `:feature:search` +# `:feature:search:impl` ## Module dependency graph @@ -11,6 +11,22 @@ config: nodePlacementStrategy: SIMPLE --- graph TB + subgraph :feature + direction TB + subgraph :feature:search + direction TB + :feature:search:api[api]:::android-library + :feature:search:impl[impl]:::android-library + end + subgraph :feature:interests + direction TB + :feature:interests:api[api]:::android-library + end + subgraph :feature:topic + direction TB + :feature:topic:api[api]:::android-library + end + end subgraph :core direction TB :core:analytics[analytics]:::android-library @@ -22,14 +38,11 @@ graph TB :core:designsystem[designsystem]:::android-library :core:domain[domain]:::android-library :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library :core:network[network]:::android-library :core:notifications[notifications]:::android-library :core:ui[ui]:::android-library end - subgraph :feature - direction TB - :feature:search[search]:::android-feature - end :core:data -.-> :core:analytics :core:data --> :core:common @@ -50,10 +63,18 @@ graph TB :core:ui --> :core:analytics :core:ui --> :core:designsystem :core:ui --> :core:model - :feature:search -.-> :core:data - :feature:search -.-> :core:designsystem - :feature:search -.-> :core:domain - :feature:search -.-> :core:ui + :feature:interests:api --> :core:navigation + :feature:search:api -.-> :core:domain + :feature:search:api --> :core:navigation + :feature:search:impl -.-> :core:designsystem + :feature:search:impl -.-> :core:domain + :feature:search:impl -.-> :core:ui + :feature:search:impl -.-> :feature:interests:api + :feature:search:impl -.-> :feature:search:api + :feature:search:impl -.-> :feature:topic:api + :feature:topic:api -.-> :core:designsystem + :feature:topic:api --> :core:navigation + :feature:topic:api -.-> :core:ui classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/search/build.gradle.kts b/feature/search/impl/build.gradle.kts similarity index 82% rename from feature/search/build.gradle.kts rename to feature/search/impl/build.gradle.kts index 5bb659c35..8425b29f3 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/impl/build.gradle.kts @@ -15,22 +15,23 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) + alias(libs.plugins.nowinandroid.android.feature.impl) alias(libs.plugins.nowinandroid.android.library.compose) alias(libs.plugins.nowinandroid.android.library.jacoco) } android { - namespace = "com.google.samples.apps.nowinandroid.feature.search" + namespace = "com.google.samples.apps.nowinandroid.feature.search.impl" } dependencies { - implementation(projects.core.data) implementation(projects.core.domain) + implementation(projects.feature.interests.api) + implementation(projects.feature.search.api) + implementation(projects.feature.topic.api) testImplementation(projects.core.testing) androidTestImplementation(libs.bundles.androidx.compose.ui.test) androidTestImplementation(projects.core.testing) -} - +} \ No newline at end of file diff --git a/feature/search/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt b/feature/search/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreenTest.kt similarity index 93% rename from feature/search/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt rename to feature/search/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreenTest.kt index a9e2fa98f..3cb93530f 100644 --- a/feature/search/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt +++ b/feature/search/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreenTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.assertCountEquals @@ -36,6 +36,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData import com.google.samples.apps.nowinandroid.core.ui.R.string +import com.google.samples.apps.nowinandroid.feature.search.api.R import org.junit.Before import org.junit.Rule import org.junit.Test @@ -70,17 +71,17 @@ class SearchScreenTest { @Before fun setup() { composeTestRule.activity.apply { - clearSearchContentDesc = getString(R.string.feature_search_clear_search_text_content_desc) - clearRecentSearchesContentDesc = getString(R.string.feature_search_clear_recent_searches_content_desc) + clearSearchContentDesc = getString(R.string.feature_search_api_clear_search_text_content_desc) + clearRecentSearchesContentDesc = getString(R.string.feature_search_api_clear_recent_searches_content_desc) followButtonContentDesc = getString(string.core_ui_interests_card_follow_button_content_desc) unfollowButtonContentDesc = getString(string.core_ui_interests_card_unfollow_button_content_desc) - topicsString = getString(R.string.feature_search_topics) - updatesString = getString(R.string.feature_search_updates) - tryAnotherSearchString = getString(R.string.feature_search_try_another_search) + - " " + getString(R.string.feature_search_interests) + " " + getString(R.string.feature_search_to_browse_topics) - searchNotReadyString = getString(R.string.feature_search_not_ready) + topicsString = getString(R.string.feature_search_api_topics) + updatesString = getString(R.string.feature_search_api_updates) + tryAnotherSearchString = getString(R.string.feature_search_api_try_another_search) + + " " + getString(R.string.feature_search_api_interests) + " " + getString(R.string.feature_search_api_to_browse_topics) + searchNotReadyString = getString(R.string.feature_search_api_not_ready) } } diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/RecentSearchQueriesUiState.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/RecentSearchQueriesUiState.kt similarity index 93% rename from feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/RecentSearchQueriesUiState.kt rename to feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/RecentSearchQueriesUiState.kt index 8aa5bb3b8..5b8516664 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/RecentSearchQueriesUiState.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/RecentSearchQueriesUiState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchResultUiState.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchResultUiState.kt similarity index 96% rename from feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchResultUiState.kt rename to feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchResultUiState.kt index aaf7dba7d..7a6f37087 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchResultUiState.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchResultUiState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreen.kt similarity index 96% rename from feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt rename to feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreen.kt index b617f98a9..bb7164f2a 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation @@ -78,7 +78,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller @@ -93,10 +93,10 @@ import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success import com.google.samples.apps.nowinandroid.core.ui.R.string import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent import com.google.samples.apps.nowinandroid.core.ui.newsFeed -import com.google.samples.apps.nowinandroid.feature.search.R as searchR +import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR @Composable -internal fun SearchRoute( +internal fun SearchScreen( onBackClick: () -> Unit, onInterestsClick: () -> Unit, onTopicClick: (String) -> Unit, @@ -211,7 +211,7 @@ fun EmptySearchResultBody( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(horizontal = 48.dp), ) { - val message = stringResource(id = searchR.string.feature_search_result_not_found, searchQuery) + val message = stringResource(id = searchR.string.feature_search_api_result_not_found, searchQuery) val start = message.indexOf(searchQuery) Text( text = AnnotatedString( @@ -229,7 +229,7 @@ fun EmptySearchResultBody( modifier = Modifier.padding(vertical = 24.dp), ) val tryAnotherSearchString = buildAnnotatedString { - append(stringResource(id = searchR.string.feature_search_try_another_search)) + append(stringResource(id = searchR.string.feature_search_api_try_another_search)) append(" ") withLink( LinkAnnotation.Clickable( @@ -245,12 +245,12 @@ fun EmptySearchResultBody( fontWeight = FontWeight.Bold, ), ) { - append(stringResource(id = searchR.string.feature_search_interests)) + append(stringResource(id = searchR.string.feature_search_api_interests)) } } append(" ") - append(stringResource(id = searchR.string.feature_search_to_browse_topics)) + append(stringResource(id = searchR.string.feature_search_api_to_browse_topics)) } Text( text = tryAnotherSearchString, @@ -273,7 +273,7 @@ private fun SearchNotReadyBody() { modifier = Modifier.padding(horizontal = 48.dp), ) { Text( - text = stringResource(id = searchR.string.feature_search_not_ready), + text = stringResource(id = searchR.string.feature_search_api_not_ready), style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, modifier = Modifier.padding(vertical = 24.dp), @@ -314,7 +314,7 @@ private fun SearchResultBody( Text( text = buildAnnotatedString { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(stringResource(id = searchR.string.feature_search_topics)) + append(stringResource(id = searchR.string.feature_search_api_topics)) } }, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -350,7 +350,7 @@ private fun SearchResultBody( Text( text = buildAnnotatedString { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(stringResource(id = searchR.string.feature_search_updates)) + append(stringResource(id = searchR.string.feature_search_api_updates)) } }, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -402,7 +402,7 @@ private fun RecentSearchesBody( Text( text = buildAnnotatedString { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(stringResource(id = searchR.string.feature_search_recent_searches)) + append(stringResource(id = searchR.string.feature_search_api_recent_searches)) } }, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -417,7 +417,7 @@ private fun RecentSearchesBody( Icon( imageVector = NiaIcons.Close, contentDescription = stringResource( - id = searchR.string.feature_search_clear_recent_searches_content_desc, + id = searchR.string.feature_search_api_clear_recent_searches_content_desc, ), tint = MaterialTheme.colorScheme.onSurface, ) @@ -491,7 +491,7 @@ private fun SearchTextField( Icon( imageVector = NiaIcons.Search, contentDescription = stringResource( - id = searchR.string.feature_search_title, + id = searchR.string.feature_search_api_title, ), tint = MaterialTheme.colorScheme.onSurface, ) @@ -506,7 +506,7 @@ private fun SearchTextField( Icon( imageVector = NiaIcons.Close, contentDescription = stringResource( - id = searchR.string.feature_search_clear_search_text_content_desc, + id = searchR.string.feature_search_api_clear_search_text_content_desc, ), tint = MaterialTheme.colorScheme.onSurface, ) diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchUiStatePreviewParameterProvider.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchUiStatePreviewParameterProvider.kt similarity index 96% rename from feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchUiStatePreviewParameterProvider.kt rename to feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchUiStatePreviewParameterProvider.kt index 257d8b68e..1bda620c6 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchUiStatePreviewParameterProvider.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchUiStatePreviewParameterProvider.kt @@ -16,7 +16,7 @@ @file:Suppress("ktlint:standard:max-line-length") -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModel.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModel.kt similarity index 98% rename from feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModel.kt rename to feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModel.kt index 36947880e..13628de70 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModel.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel 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 new file mode 100644 index 000000000..4e3baf343 --- /dev/null +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt @@ -0,0 +1,35 @@ +/* + * 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.feature.search.impl.navigation + +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchNavKey +import com.google.samples.apps.nowinandroid.feature.search.impl.SearchScreen +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic + +fun EntryProviderScope.searchEntry(navigator: Navigator) { + entry { + SearchScreen( + onBackClick = { navigator.goBack() }, + onInterestsClick = { navigator.navigate(InterestsNavKey()) }, + onTopicClick = navigator::navigateToTopic, + ) + } +} diff --git a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt b/feature/search/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModelTest.kt similarity index 93% rename from feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt rename to feature/search/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModelTest.kt index 1b866cec2..a1f089b99 100644 --- a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt +++ b/feature/search/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import androidx.lifecycle.SavedStateHandle import com.google.samples.apps.nowinandroid.core.analytics.NoOpAnalyticsHelper @@ -27,10 +27,10 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestSearchCo import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule -import com.google.samples.apps.nowinandroid.feature.search.RecentSearchQueriesUiState.Success -import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.EmptyQuery -import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.Loading -import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.SearchNotReady +import com.google.samples.apps.nowinandroid.feature.search.impl.RecentSearchQueriesUiState.Success +import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.EmptyQuery +import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.Loading +import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.SearchNotReady import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/navigation/SearchNavigation.kt b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/navigation/SearchNavigation.kt deleted file mode 100644 index 3b16e5f71..000000000 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/navigation/SearchNavigation.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2023 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.feature.search.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import com.google.samples.apps.nowinandroid.feature.search.SearchRoute -import kotlinx.serialization.Serializable - -@Serializable data object SearchRoute - -fun NavController.navigateToSearch(navOptions: NavOptions? = null) = - navigate(SearchRoute, navOptions) - -fun NavGraphBuilder.searchScreen( - onBackClick: () -> Unit, - onInterestsClick: () -> Unit, - onTopicClick: (String) -> Unit, -) { - // TODO: Handle back stack for each top-level destination. At the moment each top-level - // destination may have own search screen's back stack. - composable { - SearchRoute( - onBackClick = onBackClick, - onInterestsClick = onInterestsClick, - onTopicClick = onTopicClick, - ) - } -} diff --git a/feature/search/src/main/res/values/strings.xml b/feature/search/src/main/res/values/strings.xml deleted file mode 100644 index e11576747..000000000 --- a/feature/search/src/main/res/values/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - Search - Clear search text - Sorry, there is no content found for your search \"%1$s\" - Sorry, we are still processing the search index. Please come back later - Try another search or explorer - Interests - to browse topics - Topics - Updates - Recent searches - Clear searches - diff --git a/feature/settings/impl/.gitignore b/feature/settings/impl/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/settings/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/bookmarks/README.md b/feature/settings/impl/README.md similarity index 90% rename from feature/bookmarks/README.md rename to feature/settings/impl/README.md index 2a2ebe214..88aadd960 100644 --- a/feature/bookmarks/README.md +++ b/feature/settings/impl/README.md @@ -1,4 +1,4 @@ -# `:feature:bookmarks` +# `:feature:settings:api` ## Module dependency graph @@ -11,6 +11,13 @@ config: nodePlacementStrategy: SIMPLE --- graph TB + subgraph :feature + direction TB + subgraph :feature:settings + direction TB + :feature:settings:impl[impl]:::android-library + end + end subgraph :core direction TB :core:analytics[analytics]:::android-library @@ -25,10 +32,6 @@ graph TB :core:notifications[notifications]:::android-library :core:ui[ui]:::android-library end - subgraph :feature - direction TB - :feature:bookmarks[bookmarks]:::android-feature - end :core:data -.-> :core:analytics :core:data --> :core:common @@ -47,9 +50,9 @@ graph TB :core:ui --> :core:analytics :core:ui --> :core:designsystem :core:ui --> :core:model - :feature:bookmarks -.-> :core:data - :feature:bookmarks -.-> :core:designsystem - :feature:bookmarks -.-> :core:ui + :feature:settings:impl -.-> :core:data + :feature:settings:impl -.-> :core:designsystem + :feature:settings:impl -.-> :core:ui classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/settings/build.gradle.kts b/feature/settings/impl/build.gradle.kts similarity index 93% rename from feature/settings/build.gradle.kts rename to feature/settings/impl/build.gradle.kts index 15d65204d..d398e6103 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/impl/build.gradle.kts @@ -15,13 +15,13 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) + alias(libs.plugins.nowinandroid.android.feature.impl) alias(libs.plugins.nowinandroid.android.library.compose) alias(libs.plugins.nowinandroid.android.library.jacoco) } android { - namespace = "com.google.samples.apps.nowinandroid.feature.settings" + namespace = "com.google.samples.apps.nowinandroid.feature.settings.impl" } dependencies { diff --git a/feature/settings/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt b/feature/settings/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialogTest.kt similarity index 82% rename from feature/settings/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt rename to feature/settings/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialogTest.kt index 790b5964d..f0f69a424 100644 --- a/feature/settings/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt +++ b/feature/settings/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialogTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.settings +package com.google.samples.apps.nowinandroid.feature.settings.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.assertIsSelected @@ -23,8 +23,8 @@ import androidx.compose.ui.test.onNodeWithText import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Loading +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Success import org.junit.Rule import org.junit.Test @@ -48,7 +48,7 @@ class SettingsDialogTest { } composeTestRule - .onNodeWithText(getString(R.string.feature_settings_loading)) + .onNodeWithText(getString(R.string.feature_settings_impl_loading)) .assertExists() } @@ -71,17 +71,17 @@ class SettingsDialogTest { } // Check that all the possible settings are displayed. - composeTestRule.onNodeWithText(getString(R.string.feature_settings_brand_default)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_brand_android)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_brand_default)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_brand_android)).assertExists() composeTestRule.onNodeWithText( - getString(R.string.feature_settings_dark_mode_config_system_default), + getString(R.string.feature_settings_impl_dark_mode_config_system_default), ).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dark_mode_config_light)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dark_mode_config_dark)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dark_mode_config_light)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dark_mode_config_dark)).assertExists() // Check that the correct settings are selected. - composeTestRule.onNodeWithText(getString(R.string.feature_settings_brand_android)).assertIsSelected() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dark_mode_config_dark)).assertIsSelected() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_brand_android)).assertIsSelected() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dark_mode_config_dark)).assertIsSelected() } @Test @@ -103,12 +103,12 @@ class SettingsDialogTest { ) } - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_preference)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_yes)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_no)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_preference)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_yes)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_no)).assertExists() // Check that the correct default dynamic color setting is selected. - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_no)).assertIsSelected() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_no)).assertIsSelected() } @Test @@ -129,10 +129,10 @@ class SettingsDialogTest { ) } - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_preference)) + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_preference)) .assertDoesNotExist() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_yes)).assertDoesNotExist() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_no)).assertDoesNotExist() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_yes)).assertDoesNotExist() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_no)).assertDoesNotExist() } @Test @@ -153,10 +153,10 @@ class SettingsDialogTest { ) } - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_preference)) + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_preference)) .assertDoesNotExist() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_yes)).assertDoesNotExist() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_no)).assertDoesNotExist() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_yes)).assertDoesNotExist() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_no)).assertDoesNotExist() } @Test @@ -177,9 +177,9 @@ class SettingsDialogTest { ) } - composeTestRule.onNodeWithText(getString(R.string.feature_settings_privacy_policy)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_licenses)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_brand_guidelines)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_feedback)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_privacy_policy)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_licenses)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_brand_guidelines)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_feedback)).assertExists() } } diff --git a/feature/settings/src/main/AndroidManifest.xml b/feature/settings/impl/src/main/AndroidManifest.xml similarity index 100% rename from feature/settings/src/main/AndroidManifest.xml rename to feature/settings/impl/src/main/AndroidManifest.xml diff --git a/feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt b/feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialog.kt similarity index 88% rename from feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt rename to feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialog.kt index ad7f30f43..b2758e286 100644 --- a/feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt +++ b/feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialog.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * 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. @@ -16,7 +16,7 @@ @file:Suppress("ktlint:standard:max-line-length") -package com.google.samples.apps.nowinandroid.feature.settings +package com.google.samples.apps.nowinandroid.feature.settings.impl import android.content.Intent import androidx.compose.animation.AnimatedVisibility @@ -52,7 +52,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTextButton @@ -66,9 +66,9 @@ import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent -import com.google.samples.apps.nowinandroid.feature.settings.R.string -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success +import com.google.samples.apps.nowinandroid.feature.settings.impl.R.string +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Loading +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Success @Composable fun SettingsDialog( @@ -109,7 +109,7 @@ fun SettingsDialog( onDismissRequest = { onDismiss() }, title = { Text( - text = stringResource(string.feature_settings_title), + text = stringResource(string.feature_settings_impl_title), style = MaterialTheme.typography.titleLarge, ) }, @@ -119,7 +119,7 @@ fun SettingsDialog( when (settingsUiState) { Loading -> { Text( - text = stringResource(string.feature_settings_loading), + text = stringResource(string.feature_settings_impl_loading), modifier = Modifier.padding(vertical = 16.dp), ) } @@ -145,7 +145,7 @@ fun SettingsDialog( modifier = Modifier.padding(horizontal = 8.dp), ) { Text( - text = stringResource(string.feature_settings_dismiss_dialog_button_text), + text = stringResource(string.feature_settings_impl_dismiss_dialog_button_text), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, ) @@ -163,50 +163,50 @@ private fun ColumnScope.SettingsPanel( onChangeDynamicColorPreference: (useDynamicColor: Boolean) -> Unit, onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit, ) { - SettingsDialogSectionTitle(text = stringResource(string.feature_settings_theme)) + SettingsDialogSectionTitle(text = stringResource(string.feature_settings_impl_theme)) Column(Modifier.selectableGroup()) { SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_brand_default), + text = stringResource(string.feature_settings_impl_brand_default), selected = settings.brand == DEFAULT, onClick = { onChangeThemeBrand(DEFAULT) }, ) SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_brand_android), + text = stringResource(string.feature_settings_impl_brand_android), selected = settings.brand == ANDROID, onClick = { onChangeThemeBrand(ANDROID) }, ) } AnimatedVisibility(visible = settings.brand == DEFAULT && supportDynamicColor) { Column { - SettingsDialogSectionTitle(text = stringResource(string.feature_settings_dynamic_color_preference)) + SettingsDialogSectionTitle(text = stringResource(string.feature_settings_impl_dynamic_color_preference)) Column(Modifier.selectableGroup()) { SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_dynamic_color_yes), + text = stringResource(string.feature_settings_impl_dynamic_color_yes), selected = settings.useDynamicColor, onClick = { onChangeDynamicColorPreference(true) }, ) SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_dynamic_color_no), + text = stringResource(string.feature_settings_impl_dynamic_color_no), selected = !settings.useDynamicColor, onClick = { onChangeDynamicColorPreference(false) }, ) } } } - SettingsDialogSectionTitle(text = stringResource(string.feature_settings_dark_mode_preference)) + SettingsDialogSectionTitle(text = stringResource(string.feature_settings_impl_dark_mode_preference)) Column(Modifier.selectableGroup()) { SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_dark_mode_config_system_default), + text = stringResource(string.feature_settings_impl_dark_mode_config_system_default), selected = settings.darkThemeConfig == FOLLOW_SYSTEM, onClick = { onChangeDarkThemeConfig(FOLLOW_SYSTEM) }, ) SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_dark_mode_config_light), + text = stringResource(string.feature_settings_impl_dark_mode_config_light), selected = settings.darkThemeConfig == LIGHT, onClick = { onChangeDarkThemeConfig(LIGHT) }, ) SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_dark_mode_config_dark), + text = stringResource(string.feature_settings_impl_dark_mode_config_dark), selected = settings.darkThemeConfig == DARK, onClick = { onChangeDarkThemeConfig(DARK) }, ) @@ -262,7 +262,7 @@ private fun LinksPanel() { NiaTextButton( onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) }, ) { - Text(text = stringResource(string.feature_settings_privacy_policy)) + Text(text = stringResource(string.feature_settings_impl_privacy_policy)) } val context = LocalContext.current NiaTextButton( @@ -270,17 +270,17 @@ private fun LinksPanel() { context.startActivity(Intent(context, OssLicensesMenuActivity::class.java)) }, ) { - Text(text = stringResource(string.feature_settings_licenses)) + Text(text = stringResource(string.feature_settings_impl_licenses)) } NiaTextButton( onClick = { uriHandler.openUri(BRAND_GUIDELINES_URL) }, ) { - Text(text = stringResource(string.feature_settings_brand_guidelines)) + Text(text = stringResource(string.feature_settings_impl_brand_guidelines)) } NiaTextButton( onClick = { uriHandler.openUri(FEEDBACK_URL) }, ) { - Text(text = stringResource(string.feature_settings_feedback)) + Text(text = stringResource(string.feature_settings_impl_feedback)) } } } diff --git a/feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt b/feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModel.kt similarity index 92% rename from feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt rename to feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModel.kt index 123c84d1c..274f916d1 100644 --- a/feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt +++ b/feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModel.kt @@ -14,15 +14,15 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.settings +package com.google.samples.apps.nowinandroid.feature.settings.impl import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Loading +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Success import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.StateFlow diff --git a/feature/settings/impl/src/main/res/values/strings.xml b/feature/settings/impl/src/main/res/values/strings.xml new file mode 100644 index 000000000..18e0dcf18 --- /dev/null +++ b/feature/settings/impl/src/main/res/values/strings.xml @@ -0,0 +1,37 @@ + + + + Settings + Search + Settings + Loading… + Privacy policy + Licenses + Brand Guidelines + Feedback + Theme + Default + Android + Dark mode preference + System default + Light + Dark + Use Dynamic Color + Yes + No + OK + diff --git a/feature/settings/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModelTest.kt b/feature/settings/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModelTest.kt similarity index 90% rename from feature/settings/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModelTest.kt rename to feature/settings/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModelTest.kt index f977612a1..1d275f745 100644 --- a/feature/settings/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModelTest.kt +++ b/feature/settings/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModelTest.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.settings +package com.google.samples.apps.nowinandroid.feature.settings.impl import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Loading +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Success import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml deleted file mode 100644 index 887539bd5..000000000 --- a/feature/settings/src/main/res/values/strings.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - Settings - Search - Settings - Loading… - Privacy policy - Licenses - Brand Guidelines - Feedback - Theme - Default - Android - Dark mode preference - System default - Light - Dark - Use Dynamic Color - Yes - No - OK - diff --git a/feature/topic/api/.gitignore b/feature/topic/api/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/topic/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/topic/api/README.md b/feature/topic/api/README.md new file mode 100644 index 000000000..62103d57b --- /dev/null +++ b/feature/topic/api/README.md @@ -0,0 +1,3 @@ +# :feature:topic:api module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_feature_topic_api.svg) diff --git a/feature/topic/api/build.gradle.kts b/feature/topic/api/build.gradle.kts new file mode 100644 index 000000000..923a9b38b --- /dev/null +++ b/feature/topic/api/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright 2022 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. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.feature.api) + alias(libs.plugins.nowinandroid.android.feature.impl) + alias(libs.plugins.nowinandroid.android.library.compose) +} + +android { + namespace = "com.google.samples.apps.nowinandroid.feature.topic.api" +} \ No newline at end of file diff --git a/feature/topic/src/main/AndroidManifest.xml b/feature/topic/api/src/main/AndroidManifest.xml similarity index 100% rename from feature/topic/src/main/AndroidManifest.xml rename to feature/topic/api/src/main/AndroidManifest.xml diff --git a/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicNavKey.kt b/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicNavKey.kt new file mode 100644 index 000000000..eb10b0cdd --- /dev/null +++ b/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicNavKey.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 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.feature.topic.api.navigation + +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import kotlinx.serialization.Serializable + +@Serializable +data class TopicNavKey(val id: String) : NavKey + +fun Navigator.navigateToTopic( + topicId: String, +) { + navigate(TopicNavKey(topicId)) +} diff --git a/feature/topic/src/main/res/values/strings.xml b/feature/topic/api/src/main/res/values/strings.xml similarity index 82% rename from feature/topic/src/main/res/values/strings.xml rename to feature/topic/api/src/main/res/values/strings.xml index fe4a6dc29..1e3f376cf 100644 --- a/feature/topic/src/main/res/values/strings.xml +++ b/feature/topic/api/src/main/res/values/strings.xml @@ -15,6 +15,5 @@ limitations under the License. --> - Loading topic - Select an Interest + Loading topic diff --git a/feature/topic/impl/.gitignore b/feature/topic/impl/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/topic/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/topic/impl/README.md b/feature/topic/impl/README.md new file mode 100644 index 000000000..eee690ec0 --- /dev/null +++ b/feature/topic/impl/README.md @@ -0,0 +1,3 @@ +# :feature:topic:impl module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_feature_topic_impl.svg) diff --git a/feature/topic/build.gradle.kts b/feature/topic/impl/build.gradle.kts similarity index 85% rename from feature/topic/build.gradle.kts rename to feature/topic/impl/build.gradle.kts index bd8b59ec8..fdf37c32e 100644 --- a/feature/topic/build.gradle.kts +++ b/feature/topic/impl/build.gradle.kts @@ -15,17 +15,20 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) + alias(libs.plugins.nowinandroid.android.feature.impl) alias(libs.plugins.nowinandroid.android.library.compose) alias(libs.plugins.nowinandroid.android.library.jacoco) } android { - namespace = "com.google.samples.apps.nowinandroid.feature.topic" + namespace = "com.google.samples.apps.nowinandroid.feature.topic.impl" } dependencies { implementation(projects.core.data) + implementation(projects.feature.topic.api) + + implementation(libs.androidx.compose.material3.adaptive.navigation3) testImplementation(projects.core.testing) testImplementation(libs.robolectric) diff --git a/feature/topic/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt b/feature/topic/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreenTest.kt similarity index 95% rename from feature/topic/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt rename to feature/topic/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreenTest.kt index 2b87baf9e..5f6782160 100644 --- a/feature/topic/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt +++ b/feature/topic/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreenTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.topic +package com.google.samples.apps.nowinandroid.feature.topic.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.hasScrollToNodeAction @@ -26,6 +26,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performScrollToNode import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData +import com.google.samples.apps.nowinandroid.feature.topic.api.R import org.junit.Before import org.junit.Rule import org.junit.Test @@ -45,7 +46,7 @@ class TopicScreenTest { @Before fun setup() { composeTestRule.activity.apply { - topicLoading = getString(R.string.feature_topic_loading) + topicLoading = getString(R.string.feature_topic_api_loading) } } diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreen.kt similarity index 96% rename from feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt rename to feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreen.kt index 8ef0d786d..5c802f225 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.topic +package com.google.samples.apps.nowinandroid.feature.topic.impl import androidx.annotation.VisibleForTesting import androidx.compose.foundation.gestures.Orientation @@ -50,7 +50,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground @@ -68,7 +68,8 @@ import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider import com.google.samples.apps.nowinandroid.core.ui.userNewsResourceCardItems -import com.google.samples.apps.nowinandroid.feature.topic.R.string +import com.google.samples.apps.nowinandroid.core.ui.R as UiR +import com.google.samples.apps.nowinandroid.feature.topic.api.R as TopicR @Composable fun TopicScreen( @@ -124,7 +125,7 @@ internal fun TopicScreen( TopicUiState.Loading -> item { NiaLoadingWheel( modifier = modifier, - contentDesc = stringResource(id = string.feature_topic_loading), + contentDesc = stringResource(id = TopicR.string.feature_topic_api_loading), ) } @@ -292,7 +293,7 @@ private fun TopicToolbar( Icon( imageVector = NiaIcons.ArrowBack, contentDescription = stringResource( - id = com.google.samples.apps.nowinandroid.core.ui.R.string.core_ui_back, + id = UiR.string.core_ui_back, ), ) } diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModel.kt similarity index 98% rename from feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt rename to feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModel.kt index 8865da463..8f780f025 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.topic +package com.google.samples.apps.nowinandroid.feature.topic.impl import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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 new file mode 100644 index 000000000..b091ca701 --- /dev/null +++ b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt @@ -0,0 +1,48 @@ +/* + * 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.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.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicNavKey +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic +import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicScreen +import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicViewModel +import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicViewModel.Factory + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +fun EntryProviderScope.topicEntry(navigator: Navigator) { + entry( + metadata = ListDetailSceneStrategy.detailPane(), + ) { key -> + val id = key.id + TopicScreen( + showBackButton = true, + onBackClick = { navigator.goBack() }, + onTopicClick = navigator::navigateToTopic, + viewModel = hiltViewModel( + key = id, + ) { factory -> + factory.create(id) + }, + ) + } +} diff --git a/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt b/feature/topic/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModelTest.kt similarity index 99% rename from feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt rename to feature/topic/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModelTest.kt index 58242110d..1bbf844c1 100644 --- a/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt +++ b/feature/topic/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.topic +package com.google.samples.apps.nowinandroid.feature.topic.impl import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt deleted file mode 100644 index 69059c81d..000000000 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2022 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.feature.topic.navigation - -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptionsBuilder -import androidx.navigation.compose.composable -import androidx.navigation.toRoute -import com.google.samples.apps.nowinandroid.feature.topic.TopicScreen -import com.google.samples.apps.nowinandroid.feature.topic.TopicViewModel -import kotlinx.serialization.Serializable - -@Serializable data class TopicRoute(val id: String) - -fun NavController.navigateToTopic(topicId: String, navOptions: NavOptionsBuilder.() -> Unit = {}) { - navigate(route = TopicRoute(topicId)) { - navOptions() - } -} - -fun NavGraphBuilder.topicScreen( - showBackButton: Boolean, - onBackClick: () -> Unit, - onTopicClick: (String) -> Unit, -) { - composable { entry -> - val id = entry.toRoute().id - TopicScreen( - showBackButton = showBackButton, - onBackClick = onBackClick, - onTopicClick = onTopicClick, - viewModel = hiltViewModel( - key = id, - ) { factory -> - factory.create(id) - }, - ) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cde9691c6..c8f7d3cce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,21 +7,25 @@ androidTools = "32.0.0-beta03" androidxActivity = "1.9.3" androidxAppCompat = "1.7.0" androidxBrowser = "1.8.0" -androidxComposeBom = "2025.08.01" +androidxComposeBom = "2025.09.01" androidxComposeFoundation = "1.8.0-alpha07" androidxComposeMaterial3Adaptive = "1.1.0-rc01" +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" -androidxHiltNavigationCompose = "1.2.0" -androidxLifecycle = "2.8.7" +androidxHiltLifecycleViewModelCompose = "1.3.0-alpha02" +androidxLifecycle = "2.10.0" androidxLintGradle = "1.0.0-alpha03" +androidxLifecycleViewModelNavigation3 = "2.10.0" androidxMacroBenchmark = "1.4.1" androidxMetrics = "1.0.0-beta01" androidxNavigation = "2.8.5" +androidxNavigation3 = "1.0.0" androidxProfileinstaller = "1.4.1" +androidxSavedStateCompose = "1.3.1" androidxTestCore = "1.7.0-rc01" androidxTestExt = "1.3.0-rc01" androidxTestRules = "1.7.0-rc01" @@ -57,6 +61,7 @@ roborazzi = "1.51.0" room = "2.8.3" truth = "1.4.4" turbine = "1.2.0" +uiTestJunit4 = "1.9.0-rc01" [bundles] androidx-compose-ui-test = ["androidx-compose-ui-test", "androidx-compose-ui-testManifest"] @@ -77,6 +82,7 @@ androidx-compose-material3-navigationSuite = { group = "androidx.compose.materia androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "androidxComposeMaterial3Adaptive" } androidx-compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "androidxComposeMaterial3Adaptive" } androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "androidxComposeMaterial3Adaptive" } +androidx-compose-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3",version.ref="androidxComposeMaterial3AdaptiveNavigation3" } androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class" } androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidxComposeRuntimeTracing" } @@ -89,14 +95,18 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" } androidx-dataStore = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" } androidx-dataStore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "androidxDataStore" } -androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } +androidx-hilt-lifecycle-viewModelCompose = { group = "androidx.hilt", name = "hilt-lifecycle-viewmodel-compose", version.ref = "androidxHiltLifecycleViewModelCompose" } androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-runtimeTesting = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } +androidx-lifecycle-viewModel-testing = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-testing", version.ref = "androidxLifecycle" } +androidx-lifecycle-viewModel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "androidxLifecycleViewModelNavigation3" } androidx-lint-gradle = { group = "androidx.lint", name = "lint-gradle", version.ref = "androidxLintGradle" } androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" } -androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" } +androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "androidxNavigation3" } +androidx-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "androidxNavigation3" } +androidx-savedstate-compose = { group = "androidx.savedstate", name = "savedstate-compose", version.ref = "androidxSavedStateCompose" } androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" } androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" } androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" } @@ -159,6 +169,7 @@ firebase-performance-gradlePlugin = { group = "com.google.firebase", name = "per kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" } +androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "uiTestJunit4" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } @@ -186,7 +197,8 @@ nowinandroid-android-application-compose = { id = "nowinandroid.android.applicat nowinandroid-android-application-firebase = { id = "nowinandroid.android.application.firebase" } nowinandroid-android-application-flavors = { id = "nowinandroid.android.application.flavors" } nowinandroid-android-application-jacoco = { id = "nowinandroid.android.application.jacoco" } -nowinandroid-android-feature = { id = "nowinandroid.android.feature" } +nowinandroid-android-feature-impl = { id = "nowinandroid.android.feature.impl" } +nowinandroid-android-feature-api = { id = "nowinandroid.android.feature.api" } nowinandroid-android-library = { id = "nowinandroid.android.library" } nowinandroid-android-library-compose = { id = "nowinandroid.android.library.compose" } nowinandroid-android-library-jacoco = { id = "nowinandroid.android.library.jacoco" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 2b8c6e45c..73a1d9d6a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -59,18 +59,24 @@ include(":core:datastore-test") include(":core:designsystem") include(":core:domain") include(":core:model") +include(":core:navigation") include(":core:network") include(":core:notifications") include(":core:screenshot-testing") include(":core:testing") include(":core:ui") -include(":feature:foryou") -include(":feature:interests") -include(":feature:bookmarks") -include(":feature:topic") -include(":feature:search") -include(":feature:settings") +include(":feature:foryou:api") +include(":feature:foryou:impl") +include(":feature:interests:api") +include(":feature:interests:impl") +include(":feature:bookmarks:api") +include(":feature:bookmarks:impl") +include(":feature:topic:api") +include(":feature:topic:impl") +include(":feature:search:api") +include(":feature:search:impl") +include(":feature:settings:impl") include(":lint") include(":sync:work") include(":sync:sync-test") diff --git a/sync/sync-test/README.md b/sync/sync-test/README.md index 69f24aaea..601724ef7 100644 --- a/sync/sync-test/README.md +++ b/sync/sync-test/README.md @@ -11,6 +11,11 @@ config: nodePlacementStrategy: SIMPLE --- graph TB + subgraph :sync + direction TB + :sync:sync-test[sync-test]:::android-library + :sync:work[work]:::android-library + end subgraph :core direction TB :core:analytics[analytics]:::android-library @@ -23,11 +28,6 @@ graph TB :core:network[network]:::android-library :core:notifications[notifications]:::android-library end - subgraph :sync - direction TB - :sync:sync-test[sync-test]:::android-library - :sync:work[work]:::android-library - end :core:data -.-> :core:analytics :core:data --> :core:common diff --git a/sync/work/README.md b/sync/work/README.md index 91abef4d2..fab74f33c 100644 --- a/sync/work/README.md +++ b/sync/work/README.md @@ -11,6 +11,10 @@ config: nodePlacementStrategy: SIMPLE --- graph TB + subgraph :sync + direction TB + :sync:work[work]:::android-library + end subgraph :core direction TB :core:analytics[analytics]:::android-library @@ -23,10 +27,6 @@ graph TB :core:network[network]:::android-library :core:notifications[notifications]:::android-library end - subgraph :sync - direction TB - :sync:work[work]:::android-library - end :core:data -.-> :core:analytics :core:data --> :core:common