diff --git a/.editorconfig b/.editorconfig
index 7be3f8784f..6c8c930bca 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -5,3 +5,13 @@
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
ktlint_function_naming_ignore_when_annotated_with=Composable, Test
+ktlint_standard_backing-property-naming = disabled
+ktlint_standard_binary-expression-wrapping = disabled
+ktlint_standard_chain-method-continuation = disabled
+ktlint_standard_class-signature = disabled
+ktlint_standard_condition-wrapping = disabled
+ktlint_standard_function-expression-body = disabled
+ktlint_standard_function-literal = disabled
+ktlint_standard_function-type-modifier-spacing = disabled
+ktlint_standard_multiline-loop = disabled
+ktlint_standard_function-signature = disabled
diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties
index dbafa68cd5..a9abe496ae 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/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 433a7e4a35..0000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
-version: 2
-updates:
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "weekly"
- - package-ecosystem: "gradle"
- directory: "/"
- schedule:
- interval: "weekly"
- registries: "*"
- labels: [ "version update" ]
- groups:
- kotlin-ksp:
- patterns:
- - "org.jetbrains.kotlin:*"
- - "org.jetbrains.kotlin.jvm"
- - "com.google.devtools.ksp"
- open-pull-requests-limit: 10
-registries:
- maven-google:
- type: "maven-repository"
- url: "https://maven.google.com"
- replaces-base: true
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 265ff2ba5c..efe4ef29ae 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -10,7 +10,7 @@ Thanks for submitting a pull request. To accept your pull request we need you do
**Ensure tests pass and code is formatted correctly**
- Run local tests on the `DemoDebug` variant by running `./gradlew testDemoDebug`
-- Fix code formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply`
+- Fix code formatting: `./gradlew spotlessApply`
**Add a description**
diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 0000000000..0eec647d9a
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "local>android/.github:renovate-config"
+ ],
+ "baseBranches": [
+ "main"
+ ],
+ "gitIgnoredAuthors": [
+ "renovate[bot]@users.noreply.github.com",
+ "github-actions[bot]@users.noreply.github.com",
+ "41898282+github-actions[bot]@users.noreply.github.com"
+ ]
+}
diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml
index b858a27d74..eec554b37d 100644
--- a/.github/workflows/Build.yaml
+++ b/.github/workflows/Build.yaml
@@ -1,6 +1,7 @@
name: Build
on:
+ workflow_dispatch:
push:
branches:
- main
@@ -18,6 +19,7 @@ jobs:
permissions:
contents: write
pull-requests: write
+ security-events: write
timeout-minutes: 60
@@ -25,39 +27,28 @@ 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
- uses: actions/setup-java@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v5
with:
distribution: 'zulu'
- java-version: 17
+ java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
- validate-wrappers: true
- gradle-home-cache-cleanup: true
-
- - name: Setup Android SDK
- uses: android-actions/setup-android@v3
-
- - name: Accept licenses
- run: yes | sdkmanager --licenses || true
+ 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
+ run: ./gradlew spotlessCheck
- name: Check Dependency Guard
id: dependencyguard_verify
@@ -86,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
@@ -96,7 +110,9 @@ jobs:
continue-on-error: false
if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
run: |
- echo "::error::Screenshot tests failed, please create a PR in your fork first." && exit 1
+ echo "::error::Screenshot tests failed, please create a PR in your fork first."
+ echo "Your fork's CI will take screenshots for your fork."
+ exit 1
# Runs if previous job failed
- name: Generate new screenshots if verification failed and it's a PR
@@ -117,19 +133,8 @@ jobs:
- name: Run local tests
run: ./gradlew testDemoDebug :lint:test
- - name: Setup GMD
- run: ./gradlew :benchmarks:pixel6Api33Setup
- --info
- -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
- -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
-
- name: Build all build type and flavor permutations
- run: ./gradlew :app:assemble :benchmarks:assemble -Pandroidx.baselineprofile.skipgeneration
- -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
- -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
- -Pandroid.experimental.androidTest.numManagedDeviceShards=1
- -Pandroid.experimental.testOptions.managedDevices.maxConcurrentDevices=1
- -Pandroid.experimental.testOptions.managedDevices.setupTimeoutMinutes=5
+ run: ./gradlew :app:assemble -PminifyWithR8=false
- name: Upload build outputs (APKs)
uses: actions/upload-artifact@v4
@@ -138,14 +143,14 @@ jobs:
path: '**/build/outputs/apk/**/*.apk'
- name: Upload JVM local results (XML)
- if: always()
+ if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: local-test-results
path: '**/build/test-results/test*UnitTest/**.xml'
- name: Upload screenshot results (PNG)
- if: always()
+ if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: screenshot-test-results
@@ -155,12 +160,33 @@ jobs:
run: ./gradlew :app:lintProdRelease :app-nia-catalog:lintRelease :lint:lint
- name: Upload lint reports (HTML)
- if: always()
+ if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: lint-reports
path: '**/build/reports/lint-results-*.html'
+ - name: Upload lint reports (SARIF) for app module
+ if: ${{ !cancelled() && hashFiles('app/**/*.sarif') != '' }}
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ 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
@@ -169,7 +195,7 @@ jobs:
timeout-minutes: 55
strategy:
matrix:
- api-level: [26, 30]
+ api-level: [26, 34]
steps:
- name: Delete unnecessary tools 🔧
@@ -196,17 +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
- uses: actions/setup-java@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v5
with:
distribution: 'zulu'
- java-version: 17
+ java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
- validate-wrappers: true
- gradle-home-cache-cleanup: true
+ 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
@@ -229,7 +257,7 @@ jobs:
run: ./gradlew createDemoDebugCombinedCoverageReport
- name: Upload test reports
- if: always()
+ if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: test-reports-${{ matrix.api-level }}
@@ -238,7 +266,7 @@ jobs:
- name: Display local test coverage (only API 30)
if: matrix.api-level == 30
id: jacoco
- uses: madrapps/jacoco-report@v1.7.0
+ uses: madrapps/jacoco-report@v1.7.1
with:
title: Combined test coverage report
min-coverage-overall: 40
diff --git a/.github/workflows/NightlyBaselineProfiles.yaml b/.github/workflows/NightlyBaselineProfiles.yaml
index 288842ac7c..7e6158653c 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:
@@ -29,13 +31,18 @@ jobs:
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK 17
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 17
- 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
diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml
index 73ab89bfaf..e890bb5a70 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
@@ -26,7 +28,7 @@ jobs:
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK 17
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 17
@@ -34,8 +36,10 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
- validate-wrappers: true
- gradle-home-cache-cleanup: true
+ 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
@@ -75,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/.run/spotlessApply.run.xml b/.run/spotlessApply.run.xml
index 5c4dac833f..03bd19a6db 100644
--- a/.run/spotlessApply.run.xml
+++ b/.run/spotlessApply.run.xml
@@ -4,7 +4,6 @@
-
diff --git a/AGENT.md b/AGENT.md
new file mode 120000
index 0000000000..47dc3e3d86
--- /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 0000000000..edcfd396ec
--- /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 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 0000000000..db08820106
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+* @dturner
diff --git a/README.md b/README.md
index 1f52703239..7f7613a0d4 100644
--- a/README.md
+++ b/README.md
@@ -115,8 +115,9 @@ To run the tests execute the following gradle tasks:
(see below for explanation). To avoid this, run `recordRoborazziDemoDebug` prior to running unit tests.
- `connectedDemoDebugAndroidTest` run all instrumented tests against the `demoDebug` variant.
-**Note:** You should not run `./gradlew test` or `./gradlew connectedAndroidTest` as this will execute
-tests against _all_ build variants which is both unecessary and will result in failures as only the
+> [!NOTE]
+> You should not run `./gradlew test` or `./gradlew connectedAndroidTest` as this will execute
+tests against _all_ build variants which is both unnecessary and will result in failures as only the
`demoDebug` variant is supported. No other variants have any tests (although this might change in future).
## Screenshot tests
@@ -138,7 +139,9 @@ stored in `modulename/src/test/screenshots`.
- `compareRoborazziDemoDebug` create comparison images between failed tests and the known correct
images. These can also be found in `modulename/src/test/screenshots`.
-**Note on failing screenshot tests:** The known correct screenshots stored in this repository are recorded on CI using Linux. Other
+> [!NOTE]
+> **Note on failing screenshot tests**
+> The known correct screenshots stored in this repository are recorded on CI using Linux. Other
platforms may (and probably will) generate slightly different images, making the screenshot tests fail.
When working on a non-Linux platform, a workaround to this is to run `recordRoborazziDemoDebug` on the
`main` branch before starting work. After making changes, `verifyRoborazziDemoDebug` will identify only
@@ -187,7 +190,7 @@ Then copy the resulting baseline profile from the emulator to [`app/src/main/bas
## Compose compiler metrics
-Run the following command to get and analyse compose compiler metrics:
+Run the following command to get and analyze compose compiler metrics:
```bash
./gradlew assembleRelease -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true
diff --git a/app-nia-catalog/README.md b/app-nia-catalog/README.md
index cf6d05f4f8..8fd8dcb8d9 100644
--- a/app-nia-catalog/README.md
+++ b/app-nia-catalog/README.md
@@ -1,3 +1,58 @@
-# :app-nia-catalog module
-## Dependency graph
-
+# `:app-nia-catalog`
+
+## Module dependency graph
+
+
+```mermaid
+---
+config:
+ layout: elk
+ elk:
+ nodePlacementStrategy: SIMPLE
+---
+graph TB
+ subgraph :core
+ direction TB
+ :core:analytics[analytics]:::android-library
+ :core:designsystem[designsystem]:::android-library
+ :core:model[model]:::jvm-library
+ :core:ui[ui]:::android-library
+ end
+ :app-nia-catalog[app-nia-catalog]:::android-application
+
+ :app-nia-catalog -.-> :core:designsystem
+ :app-nia-catalog -.-> :core:ui
+ :core:ui --> :core:analytics
+ :core:ui --> :core:designsystem
+ :core:ui --> :core:model
+
+classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
+classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
+classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
+classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
+classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
+```
+
+📋 Graph legend
+
+```mermaid
+graph TB
+ application[application]:::android-application
+ feature[feature]:::android-feature
+ library[library]:::android-library
+ jvm[jvm]:::jvm-library
+
+ application -.-> feature
+ library --> jvm
+
+classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
+classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
+classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
+classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
+classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
+```
+
+
+
diff --git a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt
index 9cec85e716..1014a8f8ef 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.2
-androidx.activity:activity-ktx:1.9.2
-androidx.activity:activity:1.9.2
-androidx.annotation:annotation-experimental:1.4.0
-androidx.annotation:annotation-jvm:1.8.0
-androidx.annotation:annotation:1.8.0
+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.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.0
-androidx.collection:collection-ktx:1.4.0
-androidx.collection:collection:1.4.0
-androidx.compose.animation:animation-android:1.7.0
-androidx.compose.animation:animation-core-android:1.7.0
-androidx.compose.animation:animation-core:1.7.0
-androidx.compose.animation:animation:1.7.0
-androidx.compose.foundation:foundation-android:1.7.0
-androidx.compose.foundation:foundation-layout-android:1.7.0
-androidx.compose.foundation:foundation-layout:1.7.0
-androidx.compose.foundation:foundation:1.7.0
-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.0
-androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0
-androidx.compose.material3:material3-android:1.3.0
-androidx.compose.material3:material3:1.3.0
-androidx.compose.material:material-icons-core-android:1.7.0
-androidx.compose.material:material-icons-core:1.7.0
-androidx.compose.material:material-icons-extended-android:1.7.0
-androidx.compose.material:material-icons-extended:1.7.0
-androidx.compose.material:material-ripple-android:1.7.0
-androidx.compose.material:material-ripple:1.7.0
-androidx.compose.runtime:runtime-android:1.7.0
-androidx.compose.runtime:runtime-saveable-android:1.7.0
-androidx.compose.runtime:runtime-saveable:1.7.0
-androidx.compose.runtime:runtime:1.7.0
-androidx.compose.ui:ui-android:1.7.0
-androidx.compose.ui:ui-geometry-android:1.7.0
-androidx.compose.ui:ui-geometry:1.7.0
-androidx.compose.ui:ui-graphics-android:1.7.0
-androidx.compose.ui:ui-graphics:1.7.0
-androidx.compose.ui:ui-text-android:1.7.0
-androidx.compose.ui:ui-text:1.7.0
-androidx.compose.ui:ui-tooling-preview-android:1.7.0
-androidx.compose.ui:ui-tooling-preview:1.7.0
-androidx.compose.ui:ui-unit-android:1.7.0
-androidx.compose.ui:ui-unit:1.7.0
-androidx.compose.ui:ui-util-android:1.7.0
-androidx.compose.ui:ui-util:1.7.0
-androidx.compose.ui:ui:1.7.0
-androidx.compose:compose-bom:2024.09.00
+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.52
-com.google.dagger:dagger:2.52
-com.google.dagger:hilt-android:2.52
-com.google.dagger:hilt-core:2.52
+com.google.dagger:dagger-lint-aar:2.59
+com.google.dagger:dagger:2.59
+com.google.dagger:hilt-android:2.59
+com.google.dagger:hilt-core:2.59
com.google.guava:listenablefuture:1.0
com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0
@@ -110,14 +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.0.20
+org.jetbrains.kotlin:kotlin-stdlib-common:2.3.0
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
-org.jetbrains.kotlin:kotlin-stdlib:2.0.20
-org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1
-org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1
-org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1
-org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1
-org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.5.0
-org.jetbrains.kotlinx:kotlinx-datetime:0.5.0
+org.jetbrains.kotlin:kotlin-stdlib:2.3.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-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-nia-catalog/src/main/AndroidManifest.xml b/app-nia-catalog/src/main/AndroidManifest.xml
index 90341aac18..57286c5f1a 100644
--- a/app-nia-catalog/src/main/AndroidManifest.xml
+++ b/app-nia-catalog/src/main/AndroidManifest.xml
@@ -36,4 +36,4 @@
-
\ No newline at end of file
+
diff --git a/app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 28878a729e..9572fa6cbc 100644
--- a/app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -18,4 +18,4 @@
-
\ No newline at end of file
+
diff --git a/app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 28878a729e..9572fa6cbc 100644
--- a/app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -18,4 +18,4 @@
-
\ No newline at end of file
+
diff --git a/app-nia-catalog/src/main/res/values/strings.xml b/app-nia-catalog/src/main/res/values/strings.xml
index 69a3e1b119..0d92aba294 100644
--- a/app-nia-catalog/src/main/res/values/strings.xml
+++ b/app-nia-catalog/src/main/res/values/strings.xml
@@ -16,4 +16,4 @@
-->
NiA Catalog
-
\ No newline at end of file
+
diff --git a/app-nia-catalog/src/main/res/values/themes.xml b/app-nia-catalog/src/main/res/values/themes.xml
index 9aeeb83a6e..afcbaf5fc0 100644
--- a/app-nia-catalog/src/main/res/values/themes.xml
+++ b/app-nia-catalog/src/main/res/values/themes.xml
@@ -16,4 +16,4 @@
-->
-
\ No newline at end of file
+
diff --git a/app/README.md b/app/README.md
index a3fb4572af..bc2e9cb418 100644
--- a/app/README.md
+++ b/app/README.md
@@ -1,3 +1,178 @@
-# :app module
-## Dependency graph
-
+# `:app`
+
+## Module dependency graph
+
+
+```mermaid
+---
+config:
+ layout: elk
+ elk:
+ nodePlacementStrategy: SIMPLE
+---
+graph TB
+ subgraph :feature
+ direction TB
+ subgraph :feature:settings
+ direction TB
+ :feature:settings:impl[impl]:::android-library
+ end
+ subgraph :feature:foryou
+ direction TB
+ :feature:foryou:api[api]:::android-library
+ :feature:foryou:impl[impl]:::android-library
+ end
+ subgraph :feature:bookmarks
+ direction TB
+ :feature:bookmarks:api[api]:::android-library
+ :feature:bookmarks:impl[impl]:::android-library
+ end
+ subgraph :feature:search
+ direction TB
+ :feature:search:api[api]:::android-library
+ :feature:search:impl[impl]:::android-library
+ end
+ subgraph :feature:interests
+ direction TB
+ :feature:interests:api[api]:::android-library
+ :feature:interests:impl[impl]:::android-library
+ end
+ subgraph :feature:topic
+ direction TB
+ :feature:topic:api[api]:::android-library
+ :feature:topic:impl[impl]:::android-library
+ end
+ end
+ subgraph :sync
+ direction TB
+ :sync:work[work]:::android-library
+ end
+ subgraph :core
+ direction TB
+ :core:analytics[analytics]:::android-library
+ :core:common[common]:::jvm-library
+ :core:data[data]:::android-library
+ :core:database[database]:::android-library
+ :core:datastore[datastore]:::android-library
+ :core:datastore-proto[datastore-proto]:::jvm-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 92f3f549bf..2f02539435 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)
@@ -36,9 +36,6 @@ android {
// Custom test runner to set up Hilt dependency graph
testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
- vectorDrawables {
- useSupportLibrary = true
- }
}
buildTypes {
@@ -46,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"), "proguard-rules.pro")
+ 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.
@@ -64,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)
@@ -89,16 +89,18 @@ dependencies {
implementation(projects.sync.work)
implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.compose.material3)
+ implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.adaptive.layout)
implementation(libs.androidx.compose.material3.adaptive.navigation)
+ implementation(libs.androidx.compose.material3.adaptive.navigation3)
implementation(libs.androidx.compose.material3.windowSizeClass)
implementation(libs.androidx.compose.runtime.tracing)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
- implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.runtimeCompose)
- implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.lifecycle.viewModel.navigation3)
implementation(libs.androidx.profileinstaller)
implementation(libs.androidx.tracing.ktx)
implementation(libs.androidx.window.core)
@@ -114,22 +116,24 @@ dependencies {
kspTest(libs.hilt.compiler)
testImplementation(projects.core.dataTest)
+ testImplementation(projects.core.datastoreTest)
testImplementation(libs.hilt.android.testing)
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(kotlin("test"))
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)
baselineProfile(projects.benchmarks)
}
diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt
index e1c0a6f609..15bb60f0f8 100644
--- a/app/dependencies/prodReleaseRuntimeClasspath.txt
+++ b/app/dependencies/prodReleaseRuntimeClasspath.txt
@@ -1,177 +1,209 @@
-androidx.activity:activity-compose:1.9.2
-androidx.activity:activity-ktx:1.9.2
-androidx.activity:activity:1.9.2
-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.2
-androidx.collection:collection-ktx:1.4.2
-androidx.collection:collection:1.4.2
-androidx.compose.animation:animation-android:1.7.0
-androidx.compose.animation:animation-core-android:1.7.0
-androidx.compose.animation:animation-core:1.7.0
-androidx.compose.animation:animation:1.7.0
-androidx.compose.foundation:foundation-android:1.7.0
-androidx.compose.foundation:foundation-layout-android:1.7.0
-androidx.compose.foundation:foundation-layout:1.7.0
-androidx.compose.foundation:foundation:1.7.0
-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.0
-androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0
-androidx.compose.material3:material3-android:1.3.0
-androidx.compose.material3:material3-window-size-class-android:1.3.0
-androidx.compose.material3:material3-window-size-class:1.3.0
-androidx.compose.material3:material3:1.3.0
-androidx.compose.material:material-icons-core-android:1.7.0
-androidx.compose.material:material-icons-core:1.7.0
-androidx.compose.material:material-icons-extended-android:1.7.0
-androidx.compose.material:material-icons-extended:1.7.0
-androidx.compose.material:material-ripple-android:1.7.0
-androidx.compose.material:material-ripple:1.7.0
-androidx.compose.runtime:runtime-android:1.7.0
-androidx.compose.runtime:runtime-saveable-android:1.7.0
-androidx.compose.runtime:runtime-saveable:1.7.0
-androidx.compose.runtime:runtime-tracing:1.0.0-beta01
-androidx.compose.runtime:runtime:1.7.0
-androidx.compose.ui:ui-android:1.7.0
-androidx.compose.ui:ui-geometry-android:1.7.0
-androidx.compose.ui:ui-geometry:1.7.0
-androidx.compose.ui:ui-graphics-android:1.7.0
-androidx.compose.ui:ui-graphics:1.7.0
-androidx.compose.ui:ui-text-android:1.7.0
-androidx.compose.ui:ui-text:1.7.0
-androidx.compose.ui:ui-tooling-preview-android:1.7.0
-androidx.compose.ui:ui-tooling-preview:1.7.0
-androidx.compose.ui:ui-unit-android:1.7.0
-androidx.compose.ui:ui-unit:1.7.0
-androidx.compose.ui:ui-util-android:1.7.0
-androidx.compose.ui:ui-util:1.7.0
-androidx.compose.ui:ui:1.7.0
-androidx.compose:compose-bom:2024.09.00
+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.13.1
+androidx.core:core-ktx:1.16.0
androidx.core:core-splashscreen:1.0.1
-androidx.core:core:1.13.1
+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-core:1.0.0
-androidx.datastore:datastore-preferences-core:1.0.0
-androidx.datastore:datastore-preferences:1.0.0
-androidx.datastore:datastore:1.0.0
+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.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-service:2.8.3
-androidx.lifecycle:lifecycle-viewmodel-android:2.8.3
-androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.3
-androidx.lifecycle:lifecycle-viewmodel-compose: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.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.0
-androidx.navigation:navigation-common:2.8.0
-androidx.navigation:navigation-compose:2.8.0
-androidx.navigation:navigation-runtime-ktx:2.8.0
-androidx.navigation:navigation-runtime:2.8.0
+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.3.1
+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.work:work-runtime-ktx:2.9.0
-androidx.work:work-runtime:2.9.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
com.google.accompanist:accompanist-drawablepainter:0.32.0
-com.google.accompanist:accompanist-permissions:0.34.0
+com.google.accompanist:accompanist-permissions:0.37.0
com.google.android.datatransport:transport-api:3.2.0
com.google.android.datatransport:transport-backend-cct:3.3.0
com.google.android.datatransport:transport-runtime:3.3.0
com.google.android.gms:play-services-ads-identifier:18.0.0
-com.google.android.gms:play-services-base:18.0.1
+com.google.android.gms:play-services-base:18.5.0
com.google.android.gms:play-services-basement:18.4.0
com.google.android.gms:play-services-cloud-messaging:17.2.0
-com.google.android.gms:play-services-measurement-api:22.1.0
-com.google.android.gms:play-services-measurement-base:22.1.0
-com.google.android.gms:play-services-measurement-impl:22.1.0
-com.google.android.gms:play-services-measurement-sdk-api:22.1.0
-com.google.android.gms:play-services-measurement-sdk:22.1.0
-com.google.android.gms:play-services-measurement:22.1.0
-com.google.android.gms:play-services-oss-licenses:17.0.1
+com.google.android.gms:play-services-measurement-api:22.1.2
+com.google.android.gms:play-services-measurement-base:22.1.2
+com.google.android.gms:play-services-measurement-impl:22.1.2
+com.google.android.gms:play-services-measurement-sdk-api:22.1.2
+com.google.android.gms:play-services-measurement-sdk:22.1.2
+com.google.android.gms:play-services-measurement:22.1.2
+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.52
-com.google.dagger:dagger:2.52
-com.google.dagger:hilt-android:2.52
-com.google.dagger:hilt-core:2.52
+com.google.dagger:dagger-lint-aar:2.59
+com.google.dagger:dagger:2.59
+com.google.dagger:hilt-android:2.59
+com.google.dagger:hilt-core:2.59
com.google.errorprone:error_prone_annotations:2.26.0
com.google.firebase:firebase-abt:21.1.1
-com.google.firebase:firebase-analytics:22.1.0
+com.google.firebase:firebase-analytics:22.1.2
com.google.firebase:firebase-annotations:16.2.0
-com.google.firebase:firebase-bom:33.3.0
+com.google.firebase:firebase-bom:33.7.0
com.google.firebase:firebase-common-ktx:21.0.0
com.google.firebase:firebase-common:21.0.0
com.google.firebase:firebase-components:18.0.0
com.google.firebase:firebase-config-interop:16.0.1
-com.google.firebase:firebase-config:22.0.0
-com.google.firebase:firebase-crashlytics:19.1.0
+com.google.firebase:firebase-config:22.0.1
+com.google.firebase:firebase-crashlytics:19.3.0
com.google.firebase:firebase-datatransport:19.0.0
com.google.firebase:firebase-encoders-json:18.0.1
com.google.firebase:firebase-encoders-proto:16.0.0
@@ -180,22 +212,21 @@ com.google.firebase:firebase-iid-interop:17.1.0
com.google.firebase:firebase-installations-interop:17.2.0
com.google.firebase:firebase-installations:18.0.0
com.google.firebase:firebase-measurement-connector:20.0.1
-com.google.firebase:firebase-messaging:24.0.1
-com.google.firebase:firebase-perf:21.0.1
-com.google.firebase:firebase-sessions:2.0.4
-com.google.firebase:protolite-well-known-types:18.0.0
+com.google.firebase:firebase-messaging:24.1.0
+com.google.firebase:firebase-perf:21.0.3
+com.google.firebase:firebase-sessions:2.0.7
com.google.guava:failureaccess:1.0.1
com.google.guava:guava:31.1-android
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
com.google.j2objc:j2objc-annotations:1.3
-com.google.protobuf:protobuf-javalite:4.26.1
-com.google.protobuf:protobuf-kotlin-lite:4.26.1
-com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0
+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.retrofit2:retrofit:2.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
io.coil-kt:coil-compose-base:2.7.0
io.coil-kt:coil-compose:2.7.0
@@ -204,21 +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-stdlib-common:2.0.20
+org.jetbrains.kotlin:kotlin-stdlib-common:2.3.0
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
-org.jetbrains.kotlin:kotlin-stdlib:2.0.20
-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.kotlinx:kotlinx-datetime-jvm:0.5.0
-org.jetbrains.kotlinx:kotlinx-datetime:0.5.0
-org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3
-org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.3
-org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3
-org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.3
-org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3
+org.jetbrains.kotlin:kotlin-stdlib:2.3.0
+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.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 21b36a7fbc..8bd1545721 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='14' platformBuildVersionCode='34' compileSdkVersion='34' compileSdkVersionCodename='14'
-sdkVersion:'21'
-targetSdkVersion:'34'
+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'
@@ -105,9 +105,9 @@ application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml'
application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml'
launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon=''
+uses-library-not-required:'android.ext.adservices'
uses-library-not-required:'androidx.window.extensions'
uses-library-not-required:'androidx.window.sidecar'
-uses-library-not-required:'android.ext.adservices'
feature-group: label=''
uses-feature: name='android.hardware.faketouch'
uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 5f4922bce2..24a0b4a16a 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,9 +1,2 @@
-# Fix for Retrofit issue https://github.com/square/retrofit/issues/3751
-# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
--keep,allowobfuscation,allowshrinking interface retrofit2.Call
--keep,allowobfuscation,allowshrinking class retrofit2.Response
-
-# With R8 full mode generic signatures are stripped for classes that are not
-# kept. Suspend functions are wrapped in continuations where the type argument
-# is used.
--keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
+# 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 b15024cc78..c0eba5fd31 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
@@ -16,13 +16,17 @@
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
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
@@ -32,23 +36,24 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.NoActivityResumedException
import com.google.samples.apps.nowinandroid.MainActivity
import com.google.samples.apps.nowinandroid.R
+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 dagger.hilt.android.testing.BindValue
+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 org.junit.rules.TemporaryFolder
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.
@@ -62,39 +67,34 @@ class NavigationTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
- /**
- * Create a temporary folder used to create a Data Store file. This guarantees that
- * the file is removed in between each test, preventing a crash.
- */
- @BindValue
- @get:Rule(order = 1)
- val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
-
/**
* Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission.
*/
- @get:Rule(order = 2)
+ @get:Rule(order = 1)
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
/**
* Use the primary activity to initialize the app normally.
*/
- @get:Rule(order = 3)
+ @get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule()
@Inject
lateinit var topicsRepository: TopicsRepository
+ @Inject
+ 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()
@@ -255,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 {
@@ -264,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()
@@ -277,4 +282,44 @@ class NavigationTest {
onNodeWithTag("topic:${topic.id}").assertExists()
}
}
+
+ @Test
+ fun navigatingToTopicFromForYou_showsTopicDetails() {
+ composeTestRule.apply {
+ // Get the first news resource
+ val newsResource = runBlocking {
+ newsRepository.getNewsResources().first().first()
+ }
+
+ // Get its first topic and follow it
+ val topic = newsResource.topics.first()
+ onNodeWithText(topic.name).performClick()
+
+ // Get the news feed and scroll to the news resource
+ // Note: Possible flakiness. If the content of the news resource is long then the topic
+ // tag might not be visible meaning it cannot be clicked
+ onNodeWithTag("forYou:feed")
+ .performScrollToNode(hasTestTag("newsResourceCard:${newsResource.id}"))
+ .fetchSemanticsNode()
+ .apply {
+ val newsResourceCardNode = onNodeWithTag("newsResourceCard:${newsResource.id}")
+ .fetchSemanticsNode()
+ config[ScrollBy].action?.invoke(
+ 0f,
+ // to ensure the bottom of the card is visible,
+ // manually scroll the difference between the height of
+ // the scrolling node and the height of the card
+ (newsResourceCardNode.size.height - size.height).coerceAtLeast(0).toFloat(),
+ )
+ }
+
+ // Click the first topic tag
+ onAllNodesWithTag("topicTag:${topic.id}", useUnmergedTree = true)
+ .onFirst()
+ .performClick()
+
+ // Verify that we're on the correct topic details screen
+ onNodeWithTag("topic:${topic.id}").assertExists()
+ }
+ }
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5b22f98658..af35343423 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -41,6 +41,7 @@
diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt
index 599bd0b356..ecc23d80e0 100644
--- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt
+++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt
@@ -22,10 +22,7 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -35,21 +32,22 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.metrics.performance.JankStats
+import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
-import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
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.designsystem.theme.NiaTheme
-import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
-import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.ui.LocalTimeZone
import com.google.samples.apps.nowinandroid.ui.NiaApp
import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState
+import com.google.samples.apps.nowinandroid.util.isSystemInDarkTheme
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -81,53 +79,60 @@ class MainActivity : ComponentActivity() {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
- var uiState: MainActivityUiState by mutableStateOf(Loading)
+ // We keep this as a mutable state, so that we can track changes inside the composition.
+ // This allows us to react to dark/light mode changes.
+ var themeSettings by mutableStateOf(
+ ThemeSettings(
+ darkTheme = resources.configuration.isSystemInDarkTheme,
+ androidTheme = Loading.shouldUseAndroidTheme,
+ disableDynamicTheming = Loading.shouldDisableDynamicTheming,
+ ),
+ )
// Update the uiState
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.uiState
- .onEach { uiState = it }
- .collect()
+ combine(
+ isSystemInDarkTheme(),
+ viewModel.uiState,
+ ) { systemDark, uiState ->
+ ThemeSettings(
+ darkTheme = uiState.shouldUseDarkTheme(systemDark),
+ androidTheme = uiState.shouldUseAndroidTheme,
+ disableDynamicTheming = uiState.shouldDisableDynamicTheming,
+ )
+ }
+ .onEach { themeSettings = it }
+ .map { it.darkTheme }
+ .distinctUntilChanged()
+ .collect { darkTheme ->
+ trace("niaEdgeToEdge") {
+ // Turn off the decor fitting system windows, which allows us to handle insets,
+ // including IME animations, and go edge-to-edge.
+ // This is the same parameters as the default enableEdgeToEdge call, but we manually
+ // resolve whether or not to show dark theme using uiState, since it can be different
+ // than the configuration's dark theme value based on the user preference.
+ enableEdgeToEdge(
+ statusBarStyle = SystemBarStyle.auto(
+ lightScrim = android.graphics.Color.TRANSPARENT,
+ darkScrim = android.graphics.Color.TRANSPARENT,
+ ) { darkTheme },
+ navigationBarStyle = SystemBarStyle.auto(
+ lightScrim = lightScrim,
+ darkScrim = darkScrim,
+ ) { darkTheme },
+ )
+ }
+ }
}
}
// Keep the splash screen on-screen until the UI state is loaded. This condition is
// evaluated each time the app needs to be redrawn so it should be fast to avoid blocking
// the UI.
- splashScreen.setKeepOnScreenCondition {
- when (uiState) {
- Loading -> true
- is Success -> false
- }
- }
-
- // Turn off the decor fitting system windows, which allows us to handle insets,
- // including IME animations, and go edge-to-edge
- // This also sets up the initial system bar style based on the platform theme
- enableEdgeToEdge()
+ splashScreen.setKeepOnScreenCondition { viewModel.uiState.value.shouldKeepSplashScreen() }
setContent {
- val darkTheme = shouldUseDarkTheme(uiState)
-
- // Update the edge to edge configuration to match the theme
- // This is the same parameters as the default enableEdgeToEdge call, but we manually
- // resolve whether or not to show dark theme using uiState, since it can be different
- // than the configuration's dark theme value based on the user preference.
- DisposableEffect(darkTheme) {
- enableEdgeToEdge(
- statusBarStyle = SystemBarStyle.auto(
- android.graphics.Color.TRANSPARENT,
- android.graphics.Color.TRANSPARENT,
- ) { darkTheme },
- navigationBarStyle = SystemBarStyle.auto(
- lightScrim,
- darkScrim,
- ) { darkTheme },
- )
- onDispose {}
- }
-
val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
@@ -141,9 +146,9 @@ class MainActivity : ComponentActivity() {
LocalTimeZone provides currentTimeZone,
) {
NiaTheme(
- darkTheme = darkTheme,
- androidTheme = shouldUseAndroidTheme(uiState),
- disableDynamicTheming = shouldDisableDynamicTheming(uiState),
+ darkTheme = themeSettings.darkTheme,
+ androidTheme = themeSettings.androidTheme,
+ disableDynamicTheming = themeSettings.disableDynamicTheming,
) {
NiaApp(appState)
}
@@ -162,47 +167,6 @@ class MainActivity : ComponentActivity() {
}
}
-/**
- * Returns `true` if the Android theme should be used, as a function of the [uiState].
- */
-@Composable
-private fun shouldUseAndroidTheme(
- uiState: MainActivityUiState,
-): Boolean = when (uiState) {
- Loading -> false
- is Success -> when (uiState.userData.themeBrand) {
- ThemeBrand.DEFAULT -> false
- ThemeBrand.ANDROID -> true
- }
-}
-
-/**
- * Returns `true` if the dynamic color is disabled, as a function of the [uiState].
- */
-@Composable
-private fun shouldDisableDynamicTheming(
- uiState: MainActivityUiState,
-): Boolean = when (uiState) {
- Loading -> false
- is Success -> !uiState.userData.useDynamicColor
-}
-
-/**
- * Returns `true` if dark theme should be used, as a function of the [uiState] and the
- * current system context.
- */
-@Composable
-private fun shouldUseDarkTheme(
- uiState: MainActivityUiState,
-): Boolean = when (uiState) {
- Loading -> isSystemInDarkTheme()
- is Success -> when (uiState.userData.darkThemeConfig) {
- DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
- DarkThemeConfig.LIGHT -> false
- DarkThemeConfig.DARK -> true
- }
-}
-
/**
* The default light scrim, as defined by androidx and the platform:
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
@@ -214,3 +178,13 @@ private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
*/
private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)
+
+/**
+ * Class for the system theme settings.
+ * This wrapping class allows us to combine all the changes and prevent unnecessary recompositions.
+ */
+data class ThemeSettings(
+ val darkTheme: Boolean,
+ val androidTheme: Boolean,
+ val disableDynamicTheming: Boolean,
+)
diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt
index 09f4597a74..2d22b7d9cd 100644
--- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt
+++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt
@@ -21,6 +21,8 @@ import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
+import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
+import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
@@ -44,5 +46,40 @@ class MainActivityViewModel @Inject constructor(
sealed interface MainActivityUiState {
data object Loading : MainActivityUiState
- data class Success(val userData: UserData) : MainActivityUiState
+
+ data class Success(val userData: UserData) : MainActivityUiState {
+ override val shouldDisableDynamicTheming = !userData.useDynamicColor
+
+ override val shouldUseAndroidTheme: Boolean = when (userData.themeBrand) {
+ ThemeBrand.DEFAULT -> false
+ ThemeBrand.ANDROID -> true
+ }
+
+ override fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) =
+ when (userData.darkThemeConfig) {
+ DarkThemeConfig.FOLLOW_SYSTEM -> isSystemDarkTheme
+ DarkThemeConfig.LIGHT -> false
+ DarkThemeConfig.DARK -> true
+ }
+ }
+
+ /**
+ * Returns `true` if the state wasn't loaded yet and it should keep showing the splash screen.
+ */
+ fun shouldKeepSplashScreen() = this is Loading
+
+ /**
+ * Returns `true` if the dynamic color is disabled.
+ */
+ val shouldDisableDynamicTheming: Boolean get() = true
+
+ /**
+ * Returns `true` if the Android theme should be used.
+ */
+ val shouldUseAndroidTheme: Boolean get() = false
+
+ /**
+ * Returns `true` if dark theme should be used.
+ */
+ fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) = isSystemDarkTheme
}
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 8e3ad814a6..4975e5d65f 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
@@ -17,6 +17,9 @@
package com.google.samples.apps.nowinandroid
import android.app.Application
+import android.content.pm.ApplicationInfo
+import android.os.StrictMode
+import android.os.StrictMode.ThreadPolicy.Builder
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.google.samples.apps.nowinandroid.sync.initializers.Sync
@@ -37,10 +40,34 @@ class NiaApplication : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
+
+ setStrictModePolicy()
+
// Initialize Sync; the system responsible for keeping data in the app up to date.
Sync.initialize(context = this)
profileVerifierLogger()
}
override fun newImageLoader(): ImageLoader = imageLoader.get()
+
+ /**
+ * Return true if the application is debuggable.
+ */
+ private fun isDebuggable(): Boolean {
+ return 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
+ }
+
+ /**
+ * Set a thread policy that detects all potential problems on the main thread, such as network
+ * and disk access.
+ *
+ * If a problem is found, the offending call will be logged and the application will be killed.
+ */
+ private fun setStrictModePolicy() {
+ if (isDebuggable()) {
+ StrictMode.setThreadPolicy(
+ 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 f878c003b6..0000000000
--- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt
+++ /dev/null
@@ -1,62 +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.ForYouRoute
-import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen
-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.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 = ForYouRoute,
- modifier = modifier,
- ) {
- forYouScreen(onTopicClick = navController::navigateToInterests)
- 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 8150612730..0000000000
--- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt
+++ /dev/null
@@ -1,64 +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.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. Each of these destinations
- * can contain one or more screens (based on the window size). Navigation from one screen to the
- * next within a single destination will be handled directly in composables.
- */
-enum class TopLevelDestination(
- val selectedIcon: ImageVector,
- val unselectedIcon: ImageVector,
- @StringRes val iconTextId: Int,
- @StringRes val titleTextId: Int,
- val route: KClass<*>,
-) {
- FOR_YOU(
- selectedIcon = NiaIcons.Upcoming,
- unselectedIcon = NiaIcons.UpcomingBorder,
- iconTextId = forYouR.string.feature_foryou_title,
- titleTextId = R.string.app_name,
- route = ForYouRoute::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 0000000000..7021e4fa0c
--- /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 6cdc32bb07..bfaa27fa62 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,16 +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
@@ -59,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
@@ -70,21 +72,27 @@ 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
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun NiaApp(
appState: NiaAppState,
modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
) {
- val shouldShowGradientBackground =
- appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
+ val shouldShowGradientBackground = appState.navigationState.currentTopLevelKey == ForYouNavKey
var showSettingsDialog by rememberSaveable { mutableStateOf(false) }
NiaBackground(modifier = modifier) {
@@ -109,15 +117,17 @@ fun NiaApp(
)
}
}
+ CompositionLocalProvider(LocalSnackbarHostState provides snackbarHostState) {
+ NiaApp(
+ appState = appState,
- NiaApp(
- appState = appState,
- snackbarHostState = snackbarHostState,
- showSettingsDialog = showSettingsDialog,
- onSettingsDismissed = { showSettingsDialog = false },
- onTopAppBarActionClick = { showSettingsDialog = true },
- windowAdaptiveInfo = windowAdaptiveInfo,
- )
+ // TODO: Settings should be a dialog screen
+ showSettingsDialog = showSettingsDialog,
+ onSettingsDismissed = { showSettingsDialog = false },
+ onTopAppBarActionClick = { showSettingsDialog = true },
+ windowAdaptiveInfo = windowAdaptiveInfo,
+ )
+ }
}
}
}
@@ -130,16 +140,14 @@ fun NiaApp(
)
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(
@@ -147,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.route)
+ 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),
)
@@ -185,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
@@ -198,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) },
)
}
@@ -232,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() },
)
}
@@ -269,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 75a294c01c..9b48149806 100644
--- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt
+++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt
@@ -20,27 +20,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
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.currentBackStackEntryAsState
-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
@@ -55,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,
@@ -77,23 +68,12 @@ fun rememberNiaAppState(
@Stable
class NiaAppState(
- val navController: NavHostController,
+ val navigationState: NavigationState,
coroutineScope: CoroutineScope,
networkMonitor: NetworkMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor,
) {
- val currentDestination: NavDestination?
- @Composable get() = navController
- .currentBackStackEntryAsState().value?.destination
-
- val currentTopLevelDestination: TopLevelDestination?
- @Composable get() {
- return TopLevelDestination.entries.firstOrNull { topLevelDestination ->
- currentDestination?.hasRoute(route = topLevelDestination.route) ?: false
- }
- }
-
val isOffline = networkMonitor.isOnline
.map(Boolean::not)
.stateIn(
@@ -103,20 +83,14 @@ class NiaAppState(
)
/**
- * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the
- * route.
+ * The top level nav keys that have unread news resources.
*/
- val topLevelDestinations: List = TopLevelDestination.entries
-
- /**
- * The top level destinations 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(
@@ -131,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 3d37f34179..0000000000
--- 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 669c6300a8..0000000000
--- 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/kotlin/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt
index 595166f034..659b134b8f 100644
--- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt
+++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt
@@ -18,7 +18,7 @@ package com.google.samples.apps.nowinandroid.util
import android.util.Log
import androidx.profileinstaller.ProfileVerifier
-import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope
+import com.google.samples.apps.nowinandroid.core.common.network.di.ApplicationScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/util/UiExtensions.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/util/UiExtensions.kt
new file mode 100644
index 0000000000..20d55ab4ce
--- /dev/null
+++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/util/UiExtensions.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.util
+
+import android.content.res.Configuration
+import androidx.activity.ComponentActivity
+import androidx.core.util.Consumer
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/**
+ * Convenience wrapper for dark mode checking
+ */
+val Configuration.isSystemInDarkTheme
+ get() = (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
+
+/**
+ * Registers listener for configuration changes to retrieve whether system is in dark theme or not.
+ * Immediately upon subscribing, it sends the current value and then registers listener for changes.
+ */
+fun ComponentActivity.isSystemInDarkTheme() = callbackFlow {
+ channel.trySend(resources.configuration.isSystemInDarkTheme)
+
+ val listener = Consumer {
+ channel.trySend(it.isSystemInDarkTheme)
+ }
+
+ addOnConfigurationChangedListener(listener)
+
+ awaitClose { removeOnConfigurationChangedListener(listener) }
+}
+ .distinctUntilChanged()
+ .conflate()
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 28878a729e..9572fa6cbc 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -18,4 +18,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 28878a729e..9572fa6cbc 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -18,4 +18,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 5a3dc450f3..2b8c739cd7 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -15,9 +15,6 @@
limitations under the License.
-->
-
- #4D000000
-
#000000
#FCFCFC
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 7cdd255277..57077a2160 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -21,7 +21,9 @@
-
+