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 -![Dependency graph](../docs/images/graphs/dep_graph_app_nia_catalog.svg) +# `: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 -![Dependency graph](../docs/images/graphs/dep_graph_app.svg) +# `: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 @@