diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index eac831e90..132b64537 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -77,27 +77,28 @@ jobs: disable_globbing: true commit_message: "🤖 Updates baselines for Dependency Guard" - - name: Update Graphs - run: ./gradlew graphUpdate - - - name: Check Graphs - id: graphs_verify - 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 - continue-on-error: false - if: steps.graphs_verify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository - run: | - echo "::error::Check Graphs failed, please update graphs with: ./gradlew graphUpdate" && exit 1 - - - name: Push new graphs if available - if: steps.graphs_verify.outcome == 'failure' && github.event_name == 'pull_request' - uses: stefanzweifel/git-auto-commit-action@v5 - with: - file_pattern: '**/README.md' - disable_globbing: true - commit_message: "🤖 Updates graphs" +# See https://github.com/android/nowinandroid/issues/2005 +# - name: Update Graphs +# run: ./gradlew graphUpdate +# +# - name: Check Graphs +# id: graphs_verify +# 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 +# continue-on-error: false +# if: steps.graphs_verify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository +# run: | +# echo "::error::Check Graphs failed, please update graphs with: ./gradlew graphUpdate" && exit 1 +# +# - name: Push new graphs if available +# if: steps.graphs_verify.outcome == 'failure' && github.event_name == 'pull_request' +# uses: stefanzweifel/git-auto-commit-action@v5 +# with: +# file_pattern: '**/README.md' +# disable_globbing: true +# commit_message: "🤖 Updates graphs" - name: Run all local screenshot tests (Roborazzi) id: screenshotsverify 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 166b54907..8000fbadf 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 f45d664b7..93fe307e8 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 0f6fde884..a3f6e0a2e 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -37,8 +37,9 @@ class AndroidLibraryConventionPlugin : Plugin { extensions.configure { configureKotlinAndroid(this) - testOptions.targetSdk = 35 - lint.targetSdk = 35 + testOptions.targetSdk = 36 + lint.targetSdk = 36 + defaultConfig.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 49c2eecec..9fd446eac 100644 --- a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt @@ -30,7 +30,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 3d050d86b..709a711c2 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 2ac96e556..81f26e9db 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 { 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/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 0a6132d7d..729f9b480 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,21 +7,25 @@ androidTools = "31.13.1" 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" } @@ -158,6 +168,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" } @@ -185,7 +196,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