diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties
index dbafa68cd..a9abe496a 100644
--- a/.github/ci-gradle.properties
+++ b/.github/ci-gradle.properties
@@ -17,6 +17,8 @@
org.gradle.daemon=false
org.gradle.parallel=true
org.gradle.workers.max=2
+org.gradle.configuration-cache=true
+org.gradle.configuration-cache.parallel=true
kotlin.incremental=false
diff --git a/.github/renovate.json b/.github/renovate.json
index 053b7c7c6..0eec647d9 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -5,5 +5,10 @@
],
"baseBranches": [
"main"
+ ],
+ "gitIgnoredAuthors": [
+ "renovate[bot]@users.noreply.github.com",
+ "github-actions[bot]@users.noreply.github.com",
+ "41898282+github-actions[bot]@users.noreply.github.com"
]
-}
\ No newline at end of file
+}
diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml
index e090e2155..e31ac333a 100644
--- a/.github/workflows/Build.yaml
+++ b/.github/workflows/Build.yaml
@@ -1,6 +1,7 @@
name: Build
on:
+ workflow_dispatch:
push:
branches:
- main
@@ -26,33 +27,25 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- - name: Enable KVM group perms
- run: |
- echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
- sudo udevadm control --reload-rules
- sudo udevadm trigger --name-match=kvm
- ls /dev/kvm
-
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- - name: Set up JDK 17
+ - name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'zulu'
- java-version: 17
+ java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
-
- - name: Setup Android SDK
- uses: android-actions/setup-android@v3
-
- - name: Accept licenses
- run: yes | sdkmanager --licenses || true
+ with:
+ cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+ build-scan-publish: true
+ build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
+ build-scan-terms-of-use-agree: "yes"
- name: Check build-logic
- run: ./gradlew check -p build-logic
+ run: ./gradlew :build-logic:convention:check
- name: Check spotless
run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache
@@ -84,6 +77,29 @@ jobs:
disable_globbing: true
commit_message: "🤖 Updates baselines for Dependency Guard"
+ - name: Update Graphs
+ run: ./gradlew graphUpdate
+ continue-on-error: true
+
+ - name: Check Graphs
+ id: graphs_verify
+ run: git add -- "**/README.md" && git diff --cached --quiet --exit-code -- "**/README.md"
+
+ - 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
continue-on-error: true
@@ -118,7 +134,7 @@ jobs:
run: ./gradlew testDemoDebug :lint:test
- name: Build all build type and flavor permutations
- run: ./gradlew :app:assemble
+ run: ./gradlew :app:assemble -PminifyWithR8=false
- name: Upload build outputs (APKs)
uses: actions/upload-artifact@v4
@@ -150,11 +166,26 @@ jobs:
name: lint-reports
path: '**/build/reports/lint-results-*.html'
- - name: Upload lint reports (SARIF)
- if: ${{ !cancelled() && hashFiles('**/*.sarif') != '' }}
+ - name: Upload lint reports (SARIF) for app module
+ if: ${{ !cancelled() && hashFiles('app/**/*.sarif') != '' }}
uses: github/codeql-action/upload-sarif@v3
with:
- sarif_file: './'
+ sarif_file: './app/'
+ category: app
+
+ - name: Upload lint reports (SARIF) for app-nia-catalog module
+ if: ${{ !cancelled() && hashFiles('app-nia-catalog/**/*.sarif') != '' }}
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: './app-nia-catalog/'
+ category: app-nia-catalog
+
+ - name: Upload lint reports (SARIF) for lint module
+ if: ${{ !cancelled() && hashFiles('lint/**/*.sarif') != '' }}
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: './lint/'
+ category: lint
- name: Check badging
run: ./gradlew :app:checkProdReleaseBadging
@@ -191,14 +222,19 @@ jobs:
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- - name: Set up JDK 17
+ - name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'zulu'
- java-version: 17
+ java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
+ with:
+ cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+ build-scan-publish: true
+ build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
+ build-scan-terms-of-use-agree: "yes"
- name: Build projects and run instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
diff --git a/.github/workflows/NightlyBaselineProfiles.yaml b/.github/workflows/NightlyBaselineProfiles.yaml
index de626b0a5..43d4b73fa 100644
--- a/.github/workflows/NightlyBaselineProfiles.yaml
+++ b/.github/workflows/NightlyBaselineProfiles.yaml
@@ -1,12 +1,14 @@
name: NightlyBaselineProfiles
on:
+ workflow_dispatch:
schedule:
- cron: '42 4 * * *'
jobs:
baseline_profiles:
name: "Generate Baseline Profiles"
+ if: github.repository == 'android/nowinandroid'
runs-on: ubuntu-latest
permissions:
@@ -36,6 +38,11 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
+ with:
+ cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+ build-scan-publish: true
+ build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
+ build-scan-terms-of-use-agree: "yes"
- name: Setup Android SDK
uses: android-actions/setup-android@v3
@@ -44,7 +51,7 @@ jobs:
run: yes | sdkmanager --licenses || true
- name: Check build-logic
- run: ./gradlew check -p build-logic
+ run: ./gradlew :build-logic:convention:check
- name: Setup GMD
run: ./gradlew :benchmarks:pixel6Api33Setup
@@ -52,9 +59,8 @@ jobs:
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
- # This generates both baseline and startup profile and adds them into the generated folder
- - name: Generate Baseline Profile
- run: ./gradlew :app:generateReleaseBaselineProfile
- -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=baselineprofile
- -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
- --stacktrace
\ No newline at end of file
+ - name: Build all build type and flavor permutations including baseline profiles
+ run: ./gradlew :app:assemble
+ -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=baselineprofile
+ -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
+ -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml
index 71ee060e1..4c764a51d 100644
--- a/.github/workflows/Release.yml
+++ b/.github/workflows/Release.yml
@@ -1,12 +1,14 @@
name: GitHub Release with APKs
on:
+ workflow_dispatch:
push:
tags:
- 'v*'
jobs:
build:
+ if: github.repository == 'android/nowinandroid'
runs-on: ubuntu-latest
timeout-minutes: 120
@@ -33,6 +35,11 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
+ with:
+ cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+ build-scan-publish: true
+ build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
+ build-scan-terms-of-use-agree: "yes"
- name: Setup Android SDK
uses: android-actions/setup-android@v3
@@ -72,4 +79,4 @@ jobs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/demo/release/app-demo-release.apk
asset_name: app-demo-release.apk
- asset_content_type: application/vnd.android.package-archive
+ asset_content_type: application/vnd.android.package-archive
diff --git a/AGENT.md b/AGENT.md
new file mode 120000
index 000000000..47dc3e3d8
--- /dev/null
+++ b/AGENT.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..efb39ef19
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,58 @@
+# Now in Android Project
+
+Now in Android is a native Android mobile application written in Kotlin. It provides regular news
+about Android development. Users can choose to follow topics, be notified when new content is
+available, and bookmark items.
+
+## Architecture
+
+This project is a modern Android application that follows the official architecture guidance from Google. It is a reactive, single-activity app that uses the following:
+
+- **UI:** Built entirely with Jetpack Compose, including Material 3 components and adaptive layouts for different screen sizes.
+- **State Management:** Unidirectional Data Flow (UDF) is implemented using Kotlin Coroutines and `Flow`s. `ViewModel`s act as state holders, exposing UI state as streams of data.
+- **Dependency Injection:** Hilt is used for dependency injection throughout the app, simplifying the management of dependencies and improving testability.
+- **Navigation:** Navigation is handled by Jetpack Navigation 2 for Compose, allowing for a declarative and type-safe way to navigate between screens.
+- **Data:** The data layer is implemented using the repository pattern.
+ - **Local Data:** Room and DataStore are used for local data persistence.
+ - **Remote Data:** Retrofit and OkHttp are used for fetching data from the network.
+- **Background Processing:** WorkManager is used for deferrable background tasks.
+
+## Modules
+
+The main Android app lives in the `app/` folder. Feature modules live in `feature/` and core and shared modules in `core/`.
+
+## Commands to Build & Test
+
+The app and Android libraries have two product flavors: `demo` and `prod`, and two build types: `debug` and `release`.
+
+- Build: `./gradlew assemble{Variant}`. Typically `assembleDemoDebug`.
+- Fix linting/formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply`
+- Run local tests: `./gradlew {variant}Test`
+- Run single test: `./gradlew {variant}Test --tests "com.example.myapp.MyTestClass"`
+- Run local screenshot tests: `./gradlew verifyRoborazziDemoDebug`
+
+### Instrumented tests
+
+- Gradle-managed devices to run on device tests: `./gradlew pixel6api31aospDebugAndroidTest`. Also `pixel4api30aospatdDebugAndroidTest` and `pixelcapi30aospatdDebugAndroidTest`.
+
+### Creating tests
+
+#### Instrumented tests
+
+- Tests for UI features should only use `ComposeTestRule` with a `ComponentActivity`.
+- Bigger tests live in the `:app` module and they can start activities like `MainActivity`.
+
+#### Local tests
+
+- [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) for most assertions
+- [cashapp/turbine](https://github.com/cashapp/turbine) for complex coroutine tests
+- [google/truth](https://github.com/google/truth) for assertions
+
+## Continuous integration
+
+- The workflows are defined in `.github/workflows/*.yaml` and they contain various checks.
+- Screenshot tests are generated by CI, so they shouldn't be checked into the repo from a workstation.
+
+## Version control and code location
+
+- The project uses git and is hosted in https://github.com/android/nowinandroid.
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 000000000..db0882010
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+* @dturner
diff --git a/app-nia-catalog/README.md b/app-nia-catalog/README.md
index cf6d05f4f..8fd8dcb8d 100644
--- a/app-nia-catalog/README.md
+++ b/app-nia-catalog/README.md
@@ -1,3 +1,58 @@
-# :app-nia-catalog module
-## Dependency graph
-
+# `:app-nia-catalog`
+
+## Module dependency graph
+
+
+```mermaid
+---
+config:
+ layout: elk
+ elk:
+ nodePlacementStrategy: SIMPLE
+---
+graph TB
+ subgraph :core
+ direction TB
+ :core:analytics[analytics]:::android-library
+ :core:designsystem[designsystem]:::android-library
+ :core:model[model]:::jvm-library
+ :core:ui[ui]:::android-library
+ end
+ :app-nia-catalog[app-nia-catalog]:::android-application
+
+ :app-nia-catalog -.-> :core:designsystem
+ :app-nia-catalog -.-> :core:ui
+ :core:ui --> :core:analytics
+ :core:ui --> :core:designsystem
+ :core:ui --> :core:model
+
+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/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt
index 852b3db52..ea1e0801c 100644
--- a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt
+++ b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt
@@ -1,105 +1,121 @@
androidx.activity:activity-compose:1.9.3
androidx.activity:activity-ktx:1.9.3
androidx.activity:activity:1.9.3
-androidx.annotation:annotation-experimental:1.4.1
-androidx.annotation:annotation-jvm:1.8.1
-androidx.annotation:annotation:1.8.1
+androidx.annotation:annotation-experimental:1.5.1
+androidx.annotation:annotation-jvm:1.9.1
+androidx.annotation:annotation:1.9.1
androidx.appcompat:appcompat-resources:1.6.1
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.8.0
-androidx.collection:collection-jvm:1.4.4
-androidx.collection:collection-ktx:1.4.4
-androidx.collection:collection:1.4.4
-androidx.compose.animation:animation-android:1.7.6
-androidx.compose.animation:animation-core-android:1.7.6
-androidx.compose.animation:animation-core:1.7.6
-androidx.compose.animation:animation:1.7.6
-androidx.compose.foundation:foundation-android:1.7.6
-androidx.compose.foundation:foundation-layout-android:1.7.6
-androidx.compose.foundation:foundation-layout:1.7.6
-androidx.compose.foundation:foundation:1.7.6
-androidx.compose.material3.adaptive:adaptive-android:1.0.0
-androidx.compose.material3.adaptive:adaptive:1.0.0
-androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1
-androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1
-androidx.compose.material3:material3-android:1.3.1
-androidx.compose.material3:material3:1.3.1
-androidx.compose.material:material-icons-core-android:1.7.6
-androidx.compose.material:material-icons-core:1.7.6
-androidx.compose.material:material-icons-extended-android:1.7.6
-androidx.compose.material:material-icons-extended:1.7.6
-androidx.compose.material:material-ripple-android:1.7.6
-androidx.compose.material:material-ripple:1.7.6
-androidx.compose.runtime:runtime-android:1.7.6
-androidx.compose.runtime:runtime-saveable-android:1.7.6
-androidx.compose.runtime:runtime-saveable:1.7.6
-androidx.compose.runtime:runtime:1.7.6
-androidx.compose.ui:ui-android:1.7.6
-androidx.compose.ui:ui-geometry-android:1.7.6
-androidx.compose.ui:ui-geometry:1.7.6
-androidx.compose.ui:ui-graphics-android:1.7.6
-androidx.compose.ui:ui-graphics:1.7.6
-androidx.compose.ui:ui-text-android:1.7.6
-androidx.compose.ui:ui-text:1.7.6
-androidx.compose.ui:ui-tooling-preview-android:1.7.6
-androidx.compose.ui:ui-tooling-preview:1.7.6
-androidx.compose.ui:ui-unit-android:1.7.6
-androidx.compose.ui:ui-unit:1.7.6
-androidx.compose.ui:ui-util-android:1.7.6
-androidx.compose.ui:ui-util:1.7.6
-androidx.compose.ui:ui:1.7.6
-androidx.compose:compose-bom:2024.12.01
+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-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-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.13.1
-androidx.core:core:1.13.1
+androidx.core:core-ktx:1.16.0
+androidx.core:core-viewtree:1.0.0
+androidx.core:core:1.16.0
androidx.customview:customview-poolingcontainer:1.0.0
androidx.customview:customview:1.0.0
-androidx.emoji2:emoji2:1.3.0
+androidx.documentfile:documentfile:1.0.0
+androidx.dynamicanimation:dynamicanimation:1.0.0
+androidx.emoji2:emoji2:1.4.0
androidx.exifinterface:exifinterface:1.3.7
androidx.fragment:fragment:1.5.1
androidx.graphics:graphics-path:1.0.1
+androidx.graphics:graphics-shapes-android:1.0.1
+androidx.graphics:graphics-shapes:1.0.1
androidx.interpolator:interpolator:1.0.0
-androidx.lifecycle:lifecycle-common-java8:2.8.3
-androidx.lifecycle:lifecycle-common-jvm:2.8.3
-androidx.lifecycle:lifecycle-common:2.8.3
-androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.3
-androidx.lifecycle:lifecycle-livedata-core:2.8.3
-androidx.lifecycle:lifecycle-livedata:2.8.3
-androidx.lifecycle:lifecycle-process:2.8.3
-androidx.lifecycle:lifecycle-runtime-android:2.8.3
-androidx.lifecycle:lifecycle-runtime-compose-android:2.8.3
-androidx.lifecycle:lifecycle-runtime-compose:2.8.3
-androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.3
-androidx.lifecycle:lifecycle-runtime-ktx:2.8.3
-androidx.lifecycle:lifecycle-runtime:2.8.3
-androidx.lifecycle:lifecycle-viewmodel-android:2.8.3
-androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3
-androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.3
-androidx.lifecycle:lifecycle-viewmodel:2.8.3
+androidx.legacy:legacy-support-core-utils:1.0.0
+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.profileinstaller:profileinstaller:1.3.1
-androidx.savedstate:savedstate-ktx:1.2.1
-androidx.savedstate:savedstate:1.2.1
+androidx.print:print:1.0.0
+androidx.profileinstaller:profileinstaller:1.4.0
+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
+androidx.transition:transition:1.6.0
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.extensions.core:core:1.0.0
-androidx.window:window-core-android:1.3.0
-androidx.window:window-core:1.3.0
-androidx.window:window:1.3.0
+androidx.window:window-core-android:1.4.0
+androidx.window:window-core:1.4.0
+androidx.window:window:1.4.0
com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.code.findbugs:jsr305:3.0.2
-com.google.dagger:dagger-lint-aar:2.53.1
-com.google.dagger:dagger:2.53.1
-com.google.dagger:hilt-android:2.53.1
-com.google.dagger:hilt-core:2.53.1
+com.google.dagger:dagger-lint-aar:2.57.2
+com.google.dagger:dagger:2.57.2
+com.google.dagger:hilt-android:2.57.2
+com.google.dagger:hilt-core:2.57.2
com.google.guava:listenablefuture:1.0
com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0
@@ -110,15 +126,18 @@ io.coil-kt:coil-compose:2.7.0
io.coil-kt:coil:2.7.0
jakarta.inject:jakarta.inject-api:2.0.1
javax.inject:javax.inject:1
-org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0
+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.1.0
-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.kotlin:kotlin-stdlib:2.2.21
+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
+org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3
+org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3
org.jetbrains:annotations:23.0.0
org.jspecify:jspecify:1.0.0
diff --git a/app/README.md b/app/README.md
index a3fb4572a..cbcdc35be 100644
--- a/app/README.md
+++ b/app/README.md
@@ -1,3 +1,178 @@
-# :app module
-## Dependency graph
-
+# `:app`
+
+## Module dependency graph
+
+
+```mermaid
+---
+config:
+ layout: elk
+ elk:
+ 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
+ :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: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
+ :benchmarks[benchmarks]:::android-test
+ :app[app]:::android-application
+
+ :app -.->|baselineProfile| :benchmarks
+ :app -.-> :core:analytics
+ :app -.-> :core:common
+ :app -.-> :core:data
+ :app -.-> :core:designsystem
+ :app -.-> :core:model
+ :app -.-> :core:ui
+ :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
+ :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: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: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
+
+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/app/build.gradle.kts b/app/build.gradle.kts
index 80d3c849e..ed2223a3b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,7 +22,7 @@ plugins {
alias(libs.plugins.nowinandroid.android.application.jacoco)
alias(libs.plugins.nowinandroid.android.application.firebase)
alias(libs.plugins.nowinandroid.hilt)
- id("com.google.android.gms.oss-licenses-plugin")
+ alias(libs.plugins.google.osslicenses)
alias(libs.plugins.baselineprofile)
alias(libs.plugins.roborazzi)
alias(libs.plugins.kotlin.serialization)
@@ -43,9 +43,11 @@ android {
applicationIdSuffix = NiaBuildType.DEBUG.applicationIdSuffix
}
release {
- isMinifyEnabled = true
+ isMinifyEnabled = providers.gradleProperty("minifyWithR8")
+ .map(String::toBooleanStrict).getOrElse(true)
applicationIdSuffix = NiaBuildType.RELEASE.applicationIdSuffix
- proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro")
// To publish on the Play store a private signing key is required, but to allow anyone
// who clones the code to sign and run the release variant, use the debug signing key.
@@ -61,21 +63,22 @@ android {
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
}
}
- testOptions {
- unitTests {
- isIncludeAndroidResources = true
- }
- }
+ testOptions.unitTests.isIncludeAndroidResources = true
namespace = "com.google.samples.apps.nowinandroid"
}
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)
@@ -90,13 +93,13 @@ dependencies {
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)
@@ -117,15 +120,16 @@ dependencies {
testImplementation(projects.sync.syncTest)
testImplementation(libs.kotlin.test)
+ testDemoImplementation(libs.androidx.navigation.testing)
testDemoImplementation(libs.robolectric)
testDemoImplementation(libs.roborazzi)
testDemoImplementation(projects.core.screenshotTesting)
+ testDemoImplementation(projects.core.testing)
androidTestImplementation(projects.core.testing)
androidTestImplementation(projects.core.dataTest)
androidTestImplementation(projects.core.datastoreTest)
androidTestImplementation(libs.androidx.test.espresso.core)
- androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(libs.hilt.android.testing)
androidTestImplementation(libs.kotlin.test)
diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt
index 2db7fa46c..7bea0cb3e 100644
--- a/app/dependencies/prodReleaseRuntimeClasspath.txt
+++ b/app/dependencies/prodReleaseRuntimeClasspath.txt
@@ -1,147 +1,172 @@
-androidx.activity:activity-compose:1.9.3
-androidx.activity:activity-ktx:1.9.3
-androidx.activity:activity:1.9.3
-androidx.annotation:annotation-experimental:1.4.1
-androidx.annotation:annotation-jvm:1.8.1
-androidx.annotation:annotation:1.8.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
androidx.appcompat:appcompat-resources:1.7.0
androidx.appcompat:appcompat:1.7.0
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.8.0
-androidx.collection:collection-jvm:1.4.4
-androidx.collection:collection-ktx:1.4.4
-androidx.collection:collection:1.4.4
-androidx.compose.animation:animation-android:1.7.6
-androidx.compose.animation:animation-core-android:1.7.6
-androidx.compose.animation:animation-core:1.7.6
-androidx.compose.animation:animation:1.7.6
-androidx.compose.foundation:foundation-android:1.7.6
-androidx.compose.foundation:foundation-layout-android:1.7.6
-androidx.compose.foundation:foundation-layout:1.7.6
-androidx.compose.foundation:foundation:1.7.6
-androidx.compose.material3.adaptive:adaptive-android:1.0.0
-androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0
-androidx.compose.material3.adaptive:adaptive-layout:1.0.0
-androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0
-androidx.compose.material3.adaptive:adaptive-navigation:1.0.0
-androidx.compose.material3.adaptive:adaptive:1.0.0
-androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1
-androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1
-androidx.compose.material3:material3-android:1.3.1
-androidx.compose.material3:material3-window-size-class-android:1.3.1
-androidx.compose.material3:material3-window-size-class:1.3.1
-androidx.compose.material3:material3:1.3.1
-androidx.compose.material:material-icons-core-android:1.7.6
-androidx.compose.material:material-icons-core:1.7.6
-androidx.compose.material:material-icons-extended-android:1.7.6
-androidx.compose.material:material-icons-extended:1.7.6
-androidx.compose.material:material-ripple-android:1.7.6
-androidx.compose.material:material-ripple:1.7.6
-androidx.compose.runtime:runtime-android:1.7.6
-androidx.compose.runtime:runtime-saveable-android:1.7.6
-androidx.compose.runtime:runtime-saveable:1.7.6
-androidx.compose.runtime:runtime-tracing:1.7.6
-androidx.compose.runtime:runtime:1.7.6
-androidx.compose.ui:ui-android:1.7.6
-androidx.compose.ui:ui-geometry-android:1.7.6
-androidx.compose.ui:ui-geometry:1.7.6
-androidx.compose.ui:ui-graphics-android:1.7.6
-androidx.compose.ui:ui-graphics:1.7.6
-androidx.compose.ui:ui-text-android:1.7.6
-androidx.compose.ui:ui-text:1.7.6
-androidx.compose.ui:ui-tooling-preview-android:1.7.6
-androidx.compose.ui:ui-tooling-preview:1.7.6
-androidx.compose.ui:ui-unit-android:1.7.6
-androidx.compose.ui:ui-unit:1.7.6
-androidx.compose.ui:ui-util-android:1.7.6
-androidx.compose.ui:ui-util:1.7.6
-androidx.compose.ui:ui:1.7.6
-androidx.compose:compose-bom:2024.12.01
+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-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-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.15.0
+androidx.core:core-ktx:1.16.0
androidx.core:core-splashscreen:1.0.1
-androidx.core:core:1.15.0
+androidx.core:core-viewtree:1.0.0
+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.emoji2:emoji2-views-helper:1.3.0
-androidx.emoji2:emoji2:1.3.0
+androidx.dynamicanimation:dynamicanimation:1.0.0
+androidx.emoji2:emoji2-views-helper:1.4.0
+androidx.emoji2:emoji2:1.4.0
androidx.exifinterface:exifinterface:1.3.7
androidx.fragment:fragment:1.5.4
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.8.7
-androidx.lifecycle:lifecycle-common-jvm:2.8.7
-androidx.lifecycle:lifecycle-common:2.8.7
-androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.7
-androidx.lifecycle:lifecycle-livedata-core:2.8.7
-androidx.lifecycle:lifecycle-livedata:2.8.7
-androidx.lifecycle:lifecycle-process:2.8.7
-androidx.lifecycle:lifecycle-runtime-android:2.8.7
-androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7
-androidx.lifecycle:lifecycle-runtime-compose:2.8.7
-androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7
-androidx.lifecycle:lifecycle-runtime-ktx:2.8.7
-androidx.lifecycle:lifecycle-runtime:2.8.7
-androidx.lifecycle:lifecycle-service:2.8.7
-androidx.lifecycle:lifecycle-viewmodel-android:2.8.7
-androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.7
-androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7
-androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7
-androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7
-androidx.lifecycle:lifecycle-viewmodel:2.8.7
+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
androidx.profileinstaller:profileinstaller:1.4.1
androidx.resourceinspection:resourceinspection-annotation:1.0.1
-androidx.room:room-common:2.6.1
-androidx.room:room-ktx:2.6.1
-androidx.room:room-runtime:2.6.1
-androidx.savedstate:savedstate-ktx:1.2.1
-androidx.savedstate:savedstate:1.2.1
-androidx.sqlite:sqlite-framework:2.4.0
-androidx.sqlite:sqlite:2.4.0
+androidx.room:room-common-jvm:2.8.3
+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
+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
+androidx.sqlite:sqlite:2.6.1
androidx.startup:startup-runtime:1.1.1
androidx.tracing:tracing-ktx:1.3.0-alpha02
androidx.tracing:tracing-perfetto:1.0.0
androidx.tracing:tracing:1.3.0-alpha02
+androidx.transition:transition:1.6.0
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.extensions.core:core:1.0.0
-androidx.window:window-core-android:1.3.0
-androidx.window:window-core:1.3.0
-androidx.window:window:1.3.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
@@ -164,10 +189,10 @@ com.google.android.gms:play-services-oss-licenses:17.1.0
com.google.android.gms:play-services-stats:17.0.2
com.google.android.gms:play-services-tasks:18.2.0
com.google.code.findbugs:jsr305:3.0.2
-com.google.dagger:dagger-lint-aar:2.53.1
-com.google.dagger:dagger:2.53.1
-com.google.dagger:hilt-android:2.53.1
-com.google.dagger:hilt-core:2.53.1
+com.google.dagger:dagger-lint-aar:2.57.2
+com.google.dagger:dagger:2.57.2
+com.google.dagger:hilt-android:2.57.2
+com.google.dagger:hilt-core:2.57.2
com.google.errorprone:error_prone_annotations:2.26.0
com.google.firebase:firebase-abt:21.1.1
com.google.firebase:firebase-analytics:22.1.2
@@ -198,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
@@ -210,24 +235,22 @@ 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.1.0
+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.1.0
-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-coroutines-guava:1.9.0
-org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.9.0
+org.jetbrains.kotlin:kotlin-stdlib:2.2.21
+org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1
+org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.1
+org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.1
+org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1
+org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.1
+org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.1
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
-org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3
-org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3
-org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.3
-org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3
+org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.0
+org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.8.0
+org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.0
+org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.8.0
+org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0
org.jetbrains:annotations:23.0.0
org.jspecify:jspecify:1.0.0
diff --git a/app/prodRelease-badging.txt b/app/prodRelease-badging.txt
index 0d770604e..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'
-sdkVersion:'21'
-targetSdkVersion:'35'
+package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='16' platformBuildVersionCode='36' compileSdkVersion='36' compileSdkVersionCodename='16'
+minSdkVersion:'23'
+targetSdkVersion:'36'
uses-permission: name='android.permission.INTERNET'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.POST_NOTIFICATIONS'
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 000000000..24a0b4a16
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,2 @@
+# Repackage classes into the default package to reduce the size of descriptors.
+-repackageclasses
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/NiaApplication.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt
index 77f72e5fc..4975e5d65 100644
--- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt
+++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt
@@ -66,7 +66,7 @@ class NiaApplication : Application(), ImageLoaderFactory {
private fun setStrictModePolicy() {
if (isDebuggable()) {
StrictMode.setThreadPolicy(
- Builder().detectAll().penaltyLog().penaltyDeath().build(),
+ Builder().detectAll().penaltyLog().build(),
)
}
}
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 f27b90cbe..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
@@ -21,7 +21,9 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
@@ -31,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
@@ -58,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
@@ -69,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(
@@ -81,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) {
@@ -107,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,
+ )
+ }
}
}
}
@@ -124,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(
@@ -144,30 +155,32 @@ 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)) },
- modifier =
- Modifier
+ label = { Text(stringResource(navItem.iconTextId)) },
+ modifier = Modifier
.testTag("NiaNavItem")
.then(if (hasUnread) Modifier.notificationDot() else Modifier),
)
@@ -182,7 +195,16 @@ internal fun NiaApp(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
contentWindowInsets = WindowInsets(0, 0, 0, 0),
- snackbarHost = { SnackbarHost(snackbarHostState) },
+ snackbarHost = {
+ SnackbarHost(
+ snackbarHostState,
+ modifier = Modifier.windowInsetsPadding(
+ WindowInsets.safeDrawing.exclude(
+ WindowInsets.ime,
+ ),
+ ),
+ )
+ },
) { padding ->
Column(
Modifier
@@ -195,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.centerAlignedTopAppBarColors(
+ colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
),
onActionClick = { onTopAppBarActionClick() },
- onNavigationClick = { appState.navigateToSearch() },
+ onNavigationClick = { navigator.navigate(SearchNavKey) },
)
}
@@ -229,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() },
)
}
@@ -266,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 669c6300a..000000000
--- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt
+++ /dev/null
@@ -1,169 +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.annotation.Keep
-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.ListDetailPaneScaffold
-import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
-import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
-import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
-import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
-import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
-import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.rememberNavController
-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.navigation.TopicRoute
-import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
-import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
-import kotlinx.serialization.Serializable
-import java.util.UUID
-
-@Serializable internal object TopicPlaceholderRoute
-
-// TODO: Remove @Keep when https://issuetracker.google.com/353898971 is fixed
-@Keep
-@Serializable internal object DetailPaneNavHostRoute
-
-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
- },
- ),
- )
- BackHandler(listDetailNavigator.canNavigateBack()) {
- listDetailNavigator.navigateBack()
- }
-
- var nestedNavHostStartRoute by remember {
- val route = selectedTopicId?.let { TopicRoute(id = it) } ?: TopicPlaceholderRoute
- mutableStateOf(route)
- }
- var nestedNavKey by rememberSaveable(
- stateSaver = Saver({ it.toString() }, UUID::fromString),
- ) {
- mutableStateOf(UUID.randomUUID())
- }
- val nestedNavController = key(nestedNavKey) {
- rememberNavController()
- }
-
- fun onTopicClickShowDetailPane(topicId: String) {
- onTopicClick(topicId)
- if (listDetailNavigator.isDetailPaneVisible()) {
- // If the detail pane was visible, then use the nestedNavController navigate call
- // directly
- nestedNavController.navigateToTopic(topicId) {
- popUpTo()
- }
- } else {
- // Otherwise, recreate the NavHost entirely, and start at the new destination
- nestedNavHostStartRoute = TopicRoute(id = topicId)
- nestedNavKey = UUID.randomUUID()
- }
- listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
- }
-
- ListDetailPaneScaffold(
- value = listDetailNavigator.scaffoldValue,
- directive = listDetailNavigator.scaffoldDirective,
- listPane = {
- AnimatedPane {
- InterestsRoute(
- onTopicClick = ::onTopicClickShowDetailPane,
- highlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(),
- )
- }
- },
- detailPane = {
- AnimatedPane {
- key(nestedNavKey) {
- NavHost(
- navController = nestedNavController,
- startDestination = nestedNavHostStartRoute,
- route = DetailPaneNavHostRoute::class,
- ) {
- topicScreen(
- showBackButton = !listDetailNavigator.isListPaneVisible(),
- onBackClick = listDetailNavigator::navigateBack,
- onTopicClick = ::onTopicClickShowDetailPane,
- )
- composable {
- TopicDetailPlaceholder()
- }
- }
- }
- }
- },
- )
-}
-
-@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/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 7cdd25527..57077a216 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -21,7 +21,9 @@
-
+