diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index a4a49b8ee..7e5a80eea 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -31,7 +31,7 @@ jobs: run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 @@ -77,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 @@ -111,7 +134,7 @@ jobs: run: ./gradlew testDemoDebug :lint:test - name: Build all build type and flavor permutations - run: ./gradlew :app:assemble + run: ./gradlew :app:assemble -PminifyWithR8=false - name: Upload build outputs (APKs) uses: actions/upload-artifact@v4 @@ -200,7 +223,7 @@ jobs: run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 diff --git a/.github/workflows/NightlyBaselineProfiles.yaml b/.github/workflows/NightlyBaselineProfiles.yaml index 43d4b73fa..7e6158653 100644 --- a/.github/workflows/NightlyBaselineProfiles.yaml +++ b/.github/workflows/NightlyBaselineProfiles.yaml @@ -31,7 +31,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 diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 432154bbb..6206c3db0 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -28,7 +28,7 @@ jobs: run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 diff --git a/AGENT.md b/AGENT.md deleted file mode 100644 index efb39ef19..000000000 --- a/AGENT.md +++ /dev/null @@ -1,58 +0,0 @@ -# Now in Android Project - -Now in Android is a native Android mobile application written in Kotlin. It provides regular news -about Android development. Users can choose to follow topics, be notified when new content is -available, and bookmark items. - -## Architecture - -This project is a modern Android application that follows the official architecture guidance from Google. It is a reactive, single-activity app that uses the following: - -- **UI:** Built entirely with Jetpack Compose, including Material 3 components and adaptive layouts for different screen sizes. -- **State Management:** Unidirectional Data Flow (UDF) is implemented using Kotlin Coroutines and `Flow`s. `ViewModel`s act as state holders, exposing UI state as streams of data. -- **Dependency Injection:** Hilt is used for dependency injection throughout the app, simplifying the management of dependencies and improving testability. -- **Navigation:** Navigation is handled by Jetpack Navigation 2 for Compose, allowing for a declarative and type-safe way to navigate between screens. -- **Data:** The data layer is implemented using the repository pattern. - - **Local Data:** Room and DataStore are used for local data persistence. - - **Remote Data:** Retrofit and OkHttp are used for fetching data from the network. -- **Background Processing:** WorkManager is used for deferrable background tasks. - -## Modules - -The main Android app lives in the `app/` folder. Feature modules live in `feature/` and core and shared modules in `core/`. - -## Commands to Build & Test - -The app and Android libraries have two product flavors: `demo` and `prod`, and two build types: `debug` and `release`. - -- Build: `./gradlew assemble{Variant}`. Typically `assembleDemoDebug`. -- Fix linting/formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply` -- Run local tests: `./gradlew {variant}Test` -- Run single test: `./gradlew {variant}Test --tests "com.example.myapp.MyTestClass"` -- Run local screenshot tests: `./gradlew verifyRoborazziDemoDebug` - -### Instrumented tests - -- Gradle-managed devices to run on device tests: `./gradlew pixel6api31aospDebugAndroidTest`. Also `pixel4api30aospatdDebugAndroidTest` and `pixelcapi30aospatdDebugAndroidTest`. - -### Creating tests - -#### Instrumented tests - -- Tests for UI features should only use `ComposeTestRule` with a `ComponentActivity`. -- Bigger tests live in the `:app` module and they can start activities like `MainActivity`. - -#### Local tests - -- [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) for most assertions -- [cashapp/turbine](https://github.com/cashapp/turbine) for complex coroutine tests -- [google/truth](https://github.com/google/truth) for assertions - -## Continuous integration - -- The workflows are defined in `.github/workflows/*.yaml` and they contain various checks. -- Screenshot tests are generated by CI, so they shouldn't be checked into the repo from a workstation. - -## Version control and code location - -- The project uses git and is hosted in https://github.com/android/nowinandroid. diff --git a/AGENT.md b/AGENT.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/AGENT.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..efb39ef19 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,58 @@ +# Now in Android Project + +Now in Android is a native Android mobile application written in Kotlin. It provides regular news +about Android development. Users can choose to follow topics, be notified when new content is +available, and bookmark items. + +## Architecture + +This project is a modern Android application that follows the official architecture guidance from Google. It is a reactive, single-activity app that uses the following: + +- **UI:** Built entirely with Jetpack Compose, including Material 3 components and adaptive layouts for different screen sizes. +- **State Management:** Unidirectional Data Flow (UDF) is implemented using Kotlin Coroutines and `Flow`s. `ViewModel`s act as state holders, exposing UI state as streams of data. +- **Dependency Injection:** Hilt is used for dependency injection throughout the app, simplifying the management of dependencies and improving testability. +- **Navigation:** Navigation is handled by Jetpack Navigation 2 for Compose, allowing for a declarative and type-safe way to navigate between screens. +- **Data:** The data layer is implemented using the repository pattern. + - **Local Data:** Room and DataStore are used for local data persistence. + - **Remote Data:** Retrofit and OkHttp are used for fetching data from the network. +- **Background Processing:** WorkManager is used for deferrable background tasks. + +## Modules + +The main Android app lives in the `app/` folder. Feature modules live in `feature/` and core and shared modules in `core/`. + +## Commands to Build & Test + +The app and Android libraries have two product flavors: `demo` and `prod`, and two build types: `debug` and `release`. + +- Build: `./gradlew assemble{Variant}`. Typically `assembleDemoDebug`. +- Fix linting/formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply` +- Run local tests: `./gradlew {variant}Test` +- Run single test: `./gradlew {variant}Test --tests "com.example.myapp.MyTestClass"` +- Run local screenshot tests: `./gradlew verifyRoborazziDemoDebug` + +### Instrumented tests + +- Gradle-managed devices to run on device tests: `./gradlew pixel6api31aospDebugAndroidTest`. Also `pixel4api30aospatdDebugAndroidTest` and `pixelcapi30aospatdDebugAndroidTest`. + +### Creating tests + +#### Instrumented tests + +- Tests for UI features should only use `ComposeTestRule` with a `ComponentActivity`. +- Bigger tests live in the `:app` module and they can start activities like `MainActivity`. + +#### Local tests + +- [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) for most assertions +- [cashapp/turbine](https://github.com/cashapp/turbine) for complex coroutine tests +- [google/truth](https://github.com/google/truth) for assertions + +## Continuous integration + +- The workflows are defined in `.github/workflows/*.yaml` and they contain various checks. +- Screenshot tests are generated by CI, so they shouldn't be checked into the repo from a workstation. + +## Version control and code location + +- The project uses git and is hosted in https://github.com/android/nowinandroid. diff --git a/README.md b/README.md index 1e2dfde17..7f7613a0d 100644 --- a/README.md +++ b/README.md @@ -190,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 cf6d05f4f..8fd8dcb8d 100644 --- a/app-nia-catalog/README.md +++ b/app-nia-catalog/README.md @@ -1,3 +1,58 @@ -# :app-nia-catalog module -## Dependency graph -![Dependency graph](../docs/images/graphs/dep_graph_app_nia_catalog.svg) +# `:app-nia-catalog` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:designsystem[designsystem]:::android-library + :core:model[model]:::jvm-library + :core:ui[ui]:::android-library + end + :app-nia-catalog[app-nia-catalog]:::android-application + + :app-nia-catalog -.-> :core:designsystem + :app-nia-catalog -.-> :core:ui + :core:ui --> :core:analytics + :core:ui --> :core:designsystem + :core:ui --> :core:model + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt index 587e4feba..1014a8f8e 100644 --- a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt +++ b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt @@ -1,7 +1,7 @@ androidx.activity:activity-compose:1.9.3 androidx.activity:activity-ktx:1.9.3 androidx.activity:activity:1.9.3 -androidx.annotation:annotation-experimental:1.4.1 +androidx.annotation:annotation-experimental:1.5.1 androidx.annotation:annotation-jvm:1.9.1 androidx.annotation:annotation:1.9.1 androidx.appcompat:appcompat-resources:1.6.1 @@ -9,53 +9,60 @@ 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.5.0-beta03 -androidx.collection:collection-ktx:1.5.0-beta03 -androidx.collection:collection:1.5.0-beta03 -androidx.compose.animation:animation-android:1.8.0-beta02 -androidx.compose.animation:animation-core-android:1.8.0-beta02 -androidx.compose.animation:animation-core:1.8.0-beta02 -androidx.compose.animation:animation:1.8.0-beta02 -androidx.compose.foundation:foundation-android:1.8.0-beta02 -androidx.compose.foundation:foundation-layout-android:1.8.0-beta02 -androidx.compose.foundation:foundation-layout:1.8.0-beta02 -androidx.compose.foundation:foundation:1.8.0-beta02 -androidx.compose.material3.adaptive:adaptive-android:1.1.0-rc01 -androidx.compose.material3.adaptive:adaptive:1.1.0-rc01 -androidx.compose.material3:material3-adaptive-navigation-suite-android:1.4.0-alpha08 -androidx.compose.material3:material3-adaptive-navigation-suite:1.4.0-alpha08 -androidx.compose.material3:material3-android:1.4.0-alpha08 -androidx.compose.material3:material3:1.4.0-alpha08 +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.8.0-beta02 -androidx.compose.material:material-ripple:1.8.0-beta02 -androidx.compose.runtime:runtime-android:1.8.0-beta02 -androidx.compose.runtime:runtime-saveable-android:1.8.0-beta02 -androidx.compose.runtime:runtime-saveable:1.8.0-beta02 -androidx.compose.runtime:runtime:1.8.0-beta02 -androidx.compose.ui:ui-android:1.8.0-beta02 -androidx.compose.ui:ui-geometry-android:1.8.0-beta02 -androidx.compose.ui:ui-geometry:1.8.0-beta02 -androidx.compose.ui:ui-graphics-android:1.8.0-beta02 -androidx.compose.ui:ui-graphics:1.8.0-beta02 -androidx.compose.ui:ui-text-android:1.8.0-beta02 -androidx.compose.ui:ui-text:1.8.0-beta02 -androidx.compose.ui:ui-tooling-preview-android:1.8.0-beta02 -androidx.compose.ui:ui-tooling-preview:1.8.0-beta02 -androidx.compose.ui:ui-unit-android:1.8.0-beta02 -androidx.compose.ui:ui-unit:1.8.0-beta02 -androidx.compose.ui:ui-util-android:1.8.0-beta02 -androidx.compose.ui:ui-util:1.8.0-beta02 -androidx.compose.ui:ui:1.8.0-beta02 -androidx.compose:compose-bom-alpha:2025.02.00 +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.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 @@ -63,45 +70,52 @@ 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.7 -androidx.lifecycle:lifecycle-common-jvm:2.8.7 -androidx.lifecycle:lifecycle-common:2.8.7 -androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.7 -androidx.lifecycle:lifecycle-livedata-core:2.8.7 -androidx.lifecycle:lifecycle-livedata:2.8.7 -androidx.lifecycle:lifecycle-process:2.8.7 -androidx.lifecycle:lifecycle-runtime-android:2.8.7 -androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7 -androidx.lifecycle:lifecycle-runtime-compose:2.8.7 -androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7 -androidx.lifecycle:lifecycle-runtime-ktx:2.8.7 -androidx.lifecycle:lifecycle-runtime:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-android:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7 -androidx.lifecycle:lifecycle-viewmodel:2.8.7 +androidx.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.print:print:1.0.0 androidx.profileinstaller:profileinstaller:1.4.0 -androidx.savedstate:savedstate-ktx:1.2.1 -androidx.savedstate:savedstate:1.2.1 +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.56 -com.google.dagger:dagger:2.56 -com.google.dagger:hilt-android:2.56 -com.google.dagger:hilt-core:2.56 +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 @@ -112,15 +126,18 @@ io.coil-kt:coil-compose:2.7.0 io.coil-kt:coil:2.7.0 jakarta.inject:jakarta.inject-api:2.0.1 javax.inject:javax.inject:1 -org.jetbrains.kotlin:kotlin-stdlib-common:2.1.10 +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.1.10 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 +org.jetbrains.kotlin:kotlin-stdlib:2.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/README.md b/app/README.md index a3fb4572a..cbcdc35be 100644 --- a/app/README.md +++ b/app/README.md @@ -1,3 +1,178 @@ -# :app module -## Dependency graph -![Dependency graph](../docs/images/graphs/dep_graph_app.svg) +# `:app` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:settings + direction TB + :feature:settings:impl[impl]:::android-library + end + subgraph :feature:foryou + direction TB + :feature:foryou:api[api]:::android-library + :feature:foryou:impl[impl]:::android-library + end + subgraph :feature:bookmarks + direction TB + :feature:bookmarks:api[api]:::android-library + :feature:bookmarks:impl[impl]:::android-library + end + subgraph :feature:search + direction TB + :feature:search:api[api]:::android-library + :feature:search:impl[impl]:::android-library + end + subgraph :feature:interests + direction TB + :feature:interests:api[api]:::android-library + :feature:interests:impl[impl]:::android-library + end + subgraph :feature:topic + direction TB + :feature:topic:api[api]:::android-library + :feature:topic:impl[impl]:::android-library + end + end + subgraph :sync + direction TB + :sync:work[work]:::android-library + end + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:designsystem[designsystem]:::android-library + :core:domain[domain]:::android-library + :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + :core:ui[ui]:::android-library + end + :benchmarks[benchmarks]:::android-test + :app[app]:::android-application + + :app -.->|baselineProfile| :benchmarks + :app -.-> :core:analytics + :app -.-> :core:common + :app -.-> :core:data + :app -.-> :core:designsystem + :app -.-> :core:model + :app -.-> :core:ui + :app -.-> :feature:bookmarks:api + :app -.-> :feature:bookmarks:impl + :app -.-> :feature:foryou:api + :app -.-> :feature:foryou:impl + :app -.-> :feature:interests:api + :app -.-> :feature:interests:impl + :app -.-> :feature:search:api + :app -.-> :feature:search:impl + :app -.-> :feature:settings:impl + :app -.-> :feature:topic:api + :app -.-> :feature:topic:impl + :app -.-> :sync:work + :benchmarks -.->|testedApks| :app + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:domain --> :core:data + :core:domain --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :core:model + :core:ui --> :core:analytics + :core:ui --> :core:designsystem + :core:ui --> :core:model + :feature:bookmarks:api --> :core:navigation + :feature:bookmarks:impl -.-> :core:data + :feature:bookmarks:impl -.-> :core:designsystem + :feature:bookmarks:impl -.-> :core:ui + :feature:bookmarks:impl -.-> :feature:bookmarks:api + :feature:bookmarks:impl -.-> :feature:topic:api + :feature:foryou:api --> :core:navigation + :feature:foryou:impl -.-> :core:designsystem + :feature:foryou:impl -.-> :core:domain + :feature:foryou:impl -.-> :core:notifications + :feature:foryou:impl -.-> :core:ui + :feature:foryou:impl -.-> :feature:foryou:api + :feature:foryou:impl -.-> :feature:topic:api + :feature:interests:api --> :core:navigation + :feature:interests:impl -.-> :core:designsystem + :feature:interests:impl -.-> :core:domain + :feature:interests:impl -.-> :core:ui + :feature:interests:impl -.-> :feature:interests:api + :feature:interests:impl -.-> :feature:topic:api + :feature:search:api -.-> :core:domain + :feature:search:api --> :core:navigation + :feature:search:impl -.-> :core:designsystem + :feature:search:impl -.-> :core:domain + :feature:search:impl -.-> :core:ui + :feature:search:impl -.-> :feature:interests:api + :feature:search:impl -.-> :feature:search:api + :feature:search:impl -.-> :feature:topic:api + :feature:settings:impl -.-> :core:data + :feature:settings:impl -.-> :core:designsystem + :feature:settings:impl -.-> :core:ui + :feature:topic:api -.-> :core:designsystem + :feature:topic:api --> :core:navigation + :feature:topic:api -.-> :core:ui + :feature:topic:impl -.-> :core:data + :feature:topic:impl -.-> :core:designsystem + :feature:topic:impl -.-> :core:ui + :feature:topic:impl -.-> :feature:topic:api + :sync:work -.-> :core:analytics + :sync:work -.-> :core:data + :sync:work -.-> :core:notifications + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 682fbc1b3..2f0253943 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ plugins { alias(libs.plugins.nowinandroid.android.application.jacoco) alias(libs.plugins.nowinandroid.android.application.firebase) alias(libs.plugins.nowinandroid.hilt) - id("com.google.android.gms.oss-licenses-plugin") + alias(libs.plugins.google.osslicenses) alias(libs.plugins.baselineprofile) alias(libs.plugins.roborazzi) alias(libs.plugins.kotlin.serialization) @@ -43,7 +43,8 @@ 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") @@ -62,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) @@ -88,16 +90,17 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.material3.adaptive.layout) implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.navigation3) implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.compose.runtime.tracing) implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) - implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.lifecycle.runtimeCompose) - implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.lifecycle.viewModel.navigation3) implementation(libs.androidx.profileinstaller) implementation(libs.androidx.tracing.ktx) implementation(libs.androidx.window.core) diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt index 8457feb9f..15bb60f0f 100644 --- a/app/dependencies/prodReleaseRuntimeClasspath.txt +++ b/app/dependencies/prodReleaseRuntimeClasspath.txt @@ -1,7 +1,7 @@ -androidx.activity:activity-compose:1.9.3 -androidx.activity:activity-ktx:1.9.3 -androidx.activity:activity:1.9.3 -androidx.annotation:annotation-experimental:1.4.1 +androidx.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 @@ -10,75 +10,85 @@ 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.5.0-beta03 -androidx.collection:collection-ktx:1.5.0-beta03 -androidx.collection:collection:1.5.0-beta03 -androidx.compose.animation:animation-android:1.8.0-beta02 -androidx.compose.animation:animation-core-android:1.8.0-beta02 -androidx.compose.animation:animation-core:1.8.0-beta02 -androidx.compose.animation:animation:1.8.0-beta02 -androidx.compose.foundation:foundation-android:1.8.0-beta02 -androidx.compose.foundation:foundation-layout-android:1.8.0-beta02 -androidx.compose.foundation:foundation-layout:1.8.0-beta02 -androidx.compose.foundation:foundation:1.8.0-beta02 -androidx.compose.material3.adaptive:adaptive-android:1.1.0-rc01 -androidx.compose.material3.adaptive:adaptive-layout-android:1.1.0-rc01 -androidx.compose.material3.adaptive:adaptive-layout:1.1.0-rc01 -androidx.compose.material3.adaptive:adaptive-navigation-android:1.1.0-rc01 -androidx.compose.material3.adaptive:adaptive-navigation:1.1.0-rc01 -androidx.compose.material3.adaptive:adaptive:1.1.0-rc01 -androidx.compose.material3:material3-adaptive-navigation-suite-android:1.4.0-alpha08 -androidx.compose.material3:material3-adaptive-navigation-suite:1.4.0-alpha08 -androidx.compose.material3:material3-android:1.4.0-alpha08 -androidx.compose.material3:material3-window-size-class-android:1.4.0-alpha08 -androidx.compose.material3:material3-window-size-class:1.4.0-alpha08 -androidx.compose.material3:material3:1.4.0-alpha08 +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.8.0-beta02 -androidx.compose.material:material-ripple:1.8.0-beta02 -androidx.compose.runtime:runtime-android:1.8.0-beta02 -androidx.compose.runtime:runtime-saveable-android:1.8.0-beta02 -androidx.compose.runtime:runtime-saveable:1.8.0-beta02 -androidx.compose.runtime:runtime-tracing:1.8.0-beta02 -androidx.compose.runtime:runtime:1.8.0-beta02 -androidx.compose.ui:ui-android:1.8.0-beta02 -androidx.compose.ui:ui-geometry-android:1.8.0-beta02 -androidx.compose.ui:ui-geometry:1.8.0-beta02 -androidx.compose.ui:ui-graphics-android:1.8.0-beta02 -androidx.compose.ui:ui-graphics:1.8.0-beta02 -androidx.compose.ui:ui-text-android:1.8.0-beta02 -androidx.compose.ui:ui-text:1.8.0-beta02 -androidx.compose.ui:ui-tooling-preview-android:1.8.0-beta02 -androidx.compose.ui:ui-tooling-preview:1.8.0-beta02 -androidx.compose.ui:ui-unit-android:1.8.0-beta02 -androidx.compose.ui:ui-unit:1.8.0-beta02 -androidx.compose.ui:ui-util-android:1.8.0-beta02 -androidx.compose.ui:ui-util:1.8.0-beta02 -androidx.compose.ui:ui:1.8.0-beta02 -androidx.compose:compose-bom-alpha:2025.02.00 +androidx.compose.material:material-ripple-android:1.10.0-alpha04 +androidx.compose.material:material-ripple:1.10.0-alpha04 +androidx.compose.runtime:runtime-android:1.10.0-beta02 +androidx.compose.runtime:runtime-annotation-android:1.10.0-beta02 +androidx.compose.runtime:runtime-annotation:1.10.0-beta02 +androidx.compose.runtime:runtime-retain-android:1.10.0-beta02 +androidx.compose.runtime:runtime-retain:1.10.0-beta02 +androidx.compose.runtime:runtime-saveable-android:1.10.0-beta02 +androidx.compose.runtime:runtime-saveable:1.10.0-beta02 +androidx.compose.runtime:runtime-tracing:1.10.0-beta02 +androidx.compose.runtime:runtime:1.10.0-beta02 +androidx.compose.ui:ui-android:1.10.0-beta02 +androidx.compose.ui:ui-geometry-android:1.10.0-beta02 +androidx.compose.ui:ui-geometry:1.10.0-beta02 +androidx.compose.ui:ui-graphics-android:1.10.0-beta02 +androidx.compose.ui:ui-graphics:1.10.0-beta02 +androidx.compose.ui:ui-text-android:1.10.0-beta02 +androidx.compose.ui:ui-text:1.10.0-beta02 +androidx.compose.ui:ui-tooling-preview-android:1.10.0-beta02 +androidx.compose.ui:ui-tooling-preview:1.10.0-beta02 +androidx.compose.ui:ui-unit-android:1.10.0-beta02 +androidx.compose.ui:ui-unit:1.10.0-beta02 +androidx.compose.ui:ui-util-android:1.10.0-beta02 +androidx.compose.ui:ui-util:1.10.0-beta02 +androidx.compose.ui:ui:1.10.0-beta02 +androidx.compose:compose-bom-alpha:2025.09.01 androidx.concurrent:concurrent-futures-ktx:1.1.0 androidx.concurrent:concurrent-futures:1.1.0 -androidx.core:core-ktx:1.15.0 +androidx.core:core-ktx:1.16.0 androidx.core:core-splashscreen:1.0.1 -androidx.core:core:1.15.0 +androidx.core:core-viewtree:1.0.0 +androidx.core:core:1.16.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.0.0 -androidx.datastore:datastore-android:1.1.1 -androidx.datastore:datastore-core-android:1.1.1 -androidx.datastore:datastore-core-okio-jvm:1.1.1 -androidx.datastore:datastore-core-okio:1.1.1 -androidx.datastore:datastore-core:1.1.1 -androidx.datastore:datastore-preferences-android:1.1.1 -androidx.datastore:datastore-preferences-core-jvm:1.1.1 -androidx.datastore:datastore-preferences-core:1.1.1 -androidx.datastore:datastore-preferences:1.1.1 -androidx.datastore:datastore:1.1.1 +androidx.datastore:datastore-android:1.2.0 +androidx.datastore:datastore-core-android:1.2.0 +androidx.datastore:datastore-core-okio-jvm:1.2.0 +androidx.datastore:datastore-core-okio:1.2.0 +androidx.datastore:datastore-core:1.2.0 +androidx.datastore:datastore-preferences-android:1.2.0 +androidx.datastore:datastore-preferences-core-android:1.2.0 +androidx.datastore:datastore-preferences-core:1.2.0 +androidx.datastore:datastore-preferences-external-protobuf:1.2.0 +androidx.datastore:datastore-preferences-proto:1.2.0 +androidx.datastore:datastore-preferences:1.2.0 +androidx.datastore:datastore:1.2.0 androidx.documentfile:documentfile:1.0.0 androidx.drawerlayout:drawerlayout:1.0.0 +androidx.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 @@ -87,67 +97,76 @@ androidx.graphics:graphics-path:1.0.1 androidx.graphics:graphics-shapes-android:1.0.1 androidx.graphics:graphics-shapes:1.0.1 androidx.hilt:hilt-common:1.2.0 -androidx.hilt:hilt-navigation-compose:1.2.0 -androidx.hilt:hilt-navigation:1.2.0 +androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0-alpha02 +androidx.hilt:hilt-lifecycle-viewmodel:1.3.0-alpha02 androidx.hilt:hilt-work:1.2.0 androidx.interpolator:interpolator:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.8.7 -androidx.lifecycle:lifecycle-common-jvm:2.8.7 -androidx.lifecycle:lifecycle-common:2.8.7 -androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.7 -androidx.lifecycle:lifecycle-livedata-core:2.8.7 -androidx.lifecycle:lifecycle-livedata:2.8.7 -androidx.lifecycle:lifecycle-process:2.8.7 -androidx.lifecycle:lifecycle-runtime-android:2.8.7 -androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7 -androidx.lifecycle:lifecycle-runtime-compose:2.8.7 -androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7 -androidx.lifecycle:lifecycle-runtime-ktx:2.8.7 -androidx.lifecycle:lifecycle-runtime:2.8.7 -androidx.lifecycle:lifecycle-service:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-android:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7 -androidx.lifecycle:lifecycle-viewmodel:2.8.7 +androidx.lifecycle:lifecycle-common-java8:2.10.0 +androidx.lifecycle:lifecycle-common-jvm:2.10.0 +androidx.lifecycle:lifecycle-common:2.10.0 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0 +androidx.lifecycle:lifecycle-livedata-core:2.10.0 +androidx.lifecycle:lifecycle-livedata:2.10.0 +androidx.lifecycle:lifecycle-process:2.10.0 +androidx.lifecycle:lifecycle-runtime-android:2.10.0 +androidx.lifecycle:lifecycle-runtime-compose-android:2.10.0 +androidx.lifecycle:lifecycle-runtime-compose:2.10.0 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0 +androidx.lifecycle:lifecycle-runtime-ktx:2.10.0 +androidx.lifecycle:lifecycle-runtime:2.10.0 +androidx.lifecycle:lifecycle-service:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-android:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-compose-android:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-navigation3-android:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-navigation3:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0 +androidx.lifecycle:lifecycle-viewmodel:2.10.0 androidx.loader:loader:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.metrics:metrics-performance:1.0.0-beta01 -androidx.navigation:navigation-common-ktx:2.8.5 -androidx.navigation:navigation-common:2.8.5 -androidx.navigation:navigation-compose:2.8.5 -androidx.navigation:navigation-runtime-ktx:2.8.5 -androidx.navigation:navigation-runtime:2.8.5 +androidx.navigation3:navigation3-runtime-android:1.0.0 +androidx.navigation3:navigation3-runtime:1.0.0 +androidx.navigation3:navigation3-ui-android:1.0.0 +androidx.navigation3:navigation3-ui:1.0.0 +androidx.navigationevent:navigationevent-android:1.0.0 +androidx.navigationevent:navigationevent-compose-android:1.0.0 +androidx.navigationevent:navigationevent-compose:1.0.0 +androidx.navigationevent:navigationevent:1.0.0 androidx.print:print:1.0.0 androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05 androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05 androidx.profileinstaller:profileinstaller:1.4.1 androidx.resourceinspection:resourceinspection-annotation:1.0.1 -androidx.room:room-common-jvm:2.7.2 -androidx.room:room-common:2.7.2 -androidx.room:room-ktx:2.7.2 -androidx.room:room-runtime-android:2.7.2 -androidx.room:room-runtime:2.7.2 -androidx.savedstate:savedstate-ktx:1.2.1 -androidx.savedstate:savedstate:1.2.1 -androidx.sqlite:sqlite-android:2.5.1 -androidx.sqlite:sqlite-framework-android:2.5.1 -androidx.sqlite:sqlite-framework:2.5.1 -androidx.sqlite:sqlite:2.5.1 +androidx.room:room-common-jvm:2.8.3 +androidx.room:room-common:2.8.3 +androidx.room:room-ktx:2.8.3 +androidx.room:room-runtime-android:2.8.3 +androidx.room:room-runtime:2.8.3 +androidx.savedstate:savedstate-android:1.4.0 +androidx.savedstate:savedstate-compose-android:1.4.0 +androidx.savedstate:savedstate-compose:1.4.0 +androidx.savedstate:savedstate-ktx:1.4.0 +androidx.savedstate:savedstate:1.4.0 +androidx.sqlite:sqlite-android:2.6.1 +androidx.sqlite:sqlite-framework-android:2.6.1 +androidx.sqlite:sqlite-framework:2.6.1 +androidx.sqlite:sqlite:2.6.1 androidx.startup:startup-runtime:1.1.1 androidx.tracing:tracing-ktx:1.3.0-alpha02 androidx.tracing:tracing-perfetto:1.0.0 androidx.tracing:tracing:1.3.0-alpha02 +androidx.transition:transition:1.6.0 androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 -androidx.window.extensions.core:core:1.0.0 -androidx.window:window-core-android:1.3.0 -androidx.window:window-core:1.3.0 -androidx.window:window:1.3.0 +androidx.window:window-core-android:1.5.0 +androidx.window:window-core:1.5.0 +androidx.window:window:1.5.0 androidx.work:work-runtime-ktx:2.10.0 androidx.work:work-runtime:2.10.0 com.caverock:androidsvg-aar:1.4 @@ -170,10 +189,10 @@ com.google.android.gms:play-services-oss-licenses:17.1.0 com.google.android.gms:play-services-stats:17.0.2 com.google.android.gms:play-services-tasks:18.2.0 com.google.code.findbugs:jsr305:3.0.2 -com.google.dagger:dagger-lint-aar:2.56 -com.google.dagger:dagger:2.56 -com.google.dagger:hilt-android:2.56 -com.google.dagger:hilt-core:2.56 +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.2 @@ -204,8 +223,8 @@ com.google.protobuf:protobuf-javalite:4.29.2 com.google.protobuf:protobuf-kotlin-lite:4.29.2 com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 -com.squareup.okio:okio-jvm:3.9.0 -com.squareup.okio:okio:3.9.0 +com.squareup.okio:okio-jvm:3.9.1 +com.squareup.okio:okio:3.9.1 com.squareup.retrofit2:converter-kotlinx-serialization:2.11.0 com.squareup.retrofit2:retrofit:2.11.0 io.coil-kt:coil-base:2.7.0 @@ -216,12 +235,10 @@ io.coil-kt:coil:2.7.0 jakarta.inject:jakarta.inject-api:2.0.1 javax.inject:javax.inject:1 org.checkerframework:checker-qual:3.12.0 -org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.22 -org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.22 -org.jetbrains.kotlin:kotlin-stdlib-common:2.1.10 +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.1.10 +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 diff --git a/app/prodRelease-badging.txt b/app/prodRelease-badging.txt index ca03c1088..8bd154572 100644 --- a/app/prodRelease-badging.txt +++ b/app/prodRelease-badging.txt @@ -1,6 +1,6 @@ -package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15' -minSdkVersion:'21' -targetSdkVersion:'35' +package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='16' platformBuildVersionCode='36' compileSdkVersion='36' compileSdkVersionCodename='16' +minSdkVersion:'23' +targetSdkVersion:'36' uses-permission: name='android.permission.INTERNET' uses-permission: name='android.permission.ACCESS_NETWORK_STATE' uses-permission: name='android.permission.POST_NOTIFICATIONS' diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt index 54053a1bb..c0eba5fd3 100644 --- a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.ui import androidx.compose.ui.semantics.SemanticsActions.ScrollBy import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.hasTestTag @@ -39,18 +40,20 @@ import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule +import com.google.samples.apps.nowinandroid.feature.interests.impl.LIST_PANE_TEST_TAG import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import javax.inject.Inject -import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR -import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR -import com.google.samples.apps.nowinandroid.feature.search.R as FeatureSearchR -import com.google.samples.apps.nowinandroid.feature.settings.R as SettingsR +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as BookmarksR +import com.google.samples.apps.nowinandroid.feature.foryou.api.R as FeatureForyouR +import com.google.samples.apps.nowinandroid.feature.search.api.R as FeatureSearchR +import com.google.samples.apps.nowinandroid.feature.settings.impl.R as SettingsR /** * Tests all the navigation flows that are handled by the navigation library. @@ -83,15 +86,15 @@ class NavigationTest { lateinit var newsRepository: NewsRepository // The strings used for matching in these tests - private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_navigate_up) - private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_title) - private val interests by composeTestRule.stringResource(FeatureSearchR.string.feature_search_interests) + private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_api_navigate_up) + private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_api_title) + private val interests by composeTestRule.stringResource(FeatureSearchR.string.feature_search_api_interests) private val sampleTopic = "Headlines" private val appName by composeTestRule.stringResource(R.string.app_name) - private val saved by composeTestRule.stringResource(BookmarksR.string.feature_bookmarks_title) - private val settings by composeTestRule.stringResource(SettingsR.string.feature_settings_top_app_bar_action_icon_description) - private val brand by composeTestRule.stringResource(SettingsR.string.feature_settings_brand_android) - private val ok by composeTestRule.stringResource(SettingsR.string.feature_settings_dismiss_dialog_button_text) + private val saved by composeTestRule.stringResource(BookmarksR.string.feature_bookmarks_api_title) + private val settings by composeTestRule.stringResource(SettingsR.string.feature_settings_impl_top_app_bar_action_icon_description) + private val brand by composeTestRule.stringResource(SettingsR.string.feature_settings_impl_brand_android) + private val ok by composeTestRule.stringResource(SettingsR.string.feature_settings_impl_dismiss_dialog_button_text) @Before fun setup() = hiltRule.inject() @@ -252,6 +255,9 @@ class NavigationTest { } } + // TODO decide if backStack should preserve previous stacks when navigating back to home tab (ForYou) + // https://github.com/android/nowinandroid/issues/1937 + @Ignore @Test fun navigationBar_multipleBackStackInterests() { composeTestRule.apply { @@ -261,12 +267,14 @@ class NavigationTest { val topic = runBlocking { topicsRepository.getTopics().first().sortedBy(Topic::name).last() } - onNodeWithTag("interests:topics").performScrollToNode(hasText(topic.name)) + onNodeWithTag(LIST_PANE_TEST_TAG).performScrollToNode(hasText(topic.name)) onNodeWithText(topic.name).performClick() + // Verify the topic is still shown + onNodeWithTag("topic:${topic.id}").assertIsDisplayed() + // Switch tab onNodeWithText(forYou).performClick() - // Come back to Interests onNodeWithText(interests).performClick() diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt deleted file mode 100644 index e079c98f4..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.samples.apps.nowinandroid.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.compose.NavHost -import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouSection -import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests -import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen -import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic -import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS -import com.google.samples.apps.nowinandroid.ui.NiaAppState -import com.google.samples.apps.nowinandroid.ui.interests2pane.interestsListDetailScreen - -/** - * Top-level navigation graph. Navigation is organized as explained at - * https://d.android.com/jetpack/compose/nav-adaptive - * - * The navigation graph defined in this file defines the different top level routes. Navigation - * within each route is handled using state and Back Handlers. - */ -@Composable -fun NiaNavHost( - appState: NiaAppState, - onShowSnackbar: suspend (String, String?) -> Boolean, - modifier: Modifier = Modifier, -) { - val navController = appState.navController - NavHost( - navController = navController, - startDestination = ForYouBaseRoute, - modifier = modifier, - ) { - forYouSection( - onTopicClick = navController::navigateToTopic, - ) { - topicScreen( - showBackButton = true, - onBackClick = navController::popBackStack, - onTopicClick = navController::navigateToTopic, - ) - } - bookmarksScreen( - onTopicClick = navController::navigateToInterests, - onShowSnackbar = onShowSnackbar, - ) - searchScreen( - onBackClick = navController::popBackStack, - onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) }, - onTopicClick = navController::navigateToInterests, - ) - interestsListDetailScreen() - } -} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt deleted file mode 100644 index 429e626ff..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.samples.apps.nowinandroid.navigation - -import androidx.annotation.StringRes -import androidx.compose.ui.graphics.vector.ImageVector -import com.google.samples.apps.nowinandroid.R -import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons -import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksRoute -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute -import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute -import kotlin.reflect.KClass -import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR -import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR -import com.google.samples.apps.nowinandroid.feature.search.R as searchR - -/** - * Type for the top level destinations in the application. Contains metadata about the destination - * that is used in the top app bar and common navigation UI. - * - * @param selectedIcon The icon to be displayed in the navigation UI when this destination is - * selected. - * @param unselectedIcon The icon to be displayed in the navigation UI when this destination is - * not selected. - * @param iconTextId Text that to be displayed in the navigation UI. - * @param titleTextId Text that is displayed on the top app bar. - * @param route The route to use when navigating to this destination. - * @param baseRoute The highest ancestor of this destination. Defaults to [route], meaning that - * there is a single destination in that section of the app (no nested destinations). - */ -enum class TopLevelDestination( - val selectedIcon: ImageVector, - val unselectedIcon: ImageVector, - @StringRes val iconTextId: Int, - @StringRes val titleTextId: Int, - val route: KClass<*>, - val baseRoute: KClass<*> = route, -) { - FOR_YOU( - selectedIcon = NiaIcons.Upcoming, - unselectedIcon = NiaIcons.UpcomingBorder, - iconTextId = forYouR.string.feature_foryou_title, - titleTextId = R.string.app_name, - route = ForYouRoute::class, - baseRoute = ForYouBaseRoute::class, - ), - BOOKMARKS( - selectedIcon = NiaIcons.Bookmarks, - unselectedIcon = NiaIcons.BookmarksBorder, - iconTextId = bookmarksR.string.feature_bookmarks_title, - titleTextId = bookmarksR.string.feature_bookmarks_title, - route = BookmarksRoute::class, - ), - INTERESTS( - selectedIcon = NiaIcons.Grid3x3, - unselectedIcon = NiaIcons.Grid3x3, - iconTextId = searchR.string.feature_search_interests, - titleTextId = searchR.string.feature_search_interests, - route = InterestsRoute::class, - ), -} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt new file mode 100644 index 000000000..7021e4fa0 --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.navigation + +import androidx.annotation.StringRes +import androidx.compose.ui.graphics.vector.ImageVector +import com.google.samples.apps.nowinandroid.R +import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey +import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as bookmarksR +import com.google.samples.apps.nowinandroid.feature.foryou.api.R as forYouR +import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR + +/** + * Type for the top level navigation items in the application. Contains UI information about the + * current route that is used in the top app bar and common navigation UI. + * + * @param selectedIcon The icon to be displayed in the navigation UI when this destination is + * selected. + * @param unselectedIcon The icon to be displayed in the navigation UI when this destination is + * not selected. + * @param iconTextId Text that to be displayed in the navigation UI. + * @param titleTextId Text that is displayed on the top app bar. + */ +data class TopLevelNavItem( + val selectedIcon: ImageVector, + val unselectedIcon: ImageVector, + @StringRes val iconTextId: Int, + @StringRes val titleTextId: Int, +) + +val FOR_YOU = TopLevelNavItem( + selectedIcon = NiaIcons.Upcoming, + unselectedIcon = NiaIcons.UpcomingBorder, + iconTextId = forYouR.string.feature_foryou_api_title, + titleTextId = R.string.app_name, +) + +val BOOKMARKS = TopLevelNavItem( + selectedIcon = NiaIcons.Bookmarks, + unselectedIcon = NiaIcons.BookmarksBorder, + iconTextId = bookmarksR.string.feature_bookmarks_api_title, + titleTextId = bookmarksR.string.feature_bookmarks_api_title, +) + +val INTERESTS = TopLevelNavItem( + selectedIcon = NiaIcons.Grid3x3, + unselectedIcon = NiaIcons.Grid3x3, + iconTextId = searchR.string.feature_search_api_interests, + titleTextId = searchR.string.feature_search_api_interests, +) + +val TOP_LEVEL_NAV_ITEMS = mapOf( + ForYouNavKey to FOR_YOU, + BookmarksNavKey to BOOKMARKS, + InterestsNavKey(null) to INTERESTS, +) diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index b237684ef..bfaa27fa6 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -33,15 +33,16 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration.Indefinite -import androidx.compose.material3.SnackbarDuration.Short import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -60,9 +61,9 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavDestination -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground @@ -71,11 +72,19 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAp import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors -import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog -import com.google.samples.apps.nowinandroid.navigation.NiaNavHost -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination -import kotlin.reflect.KClass -import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.core.navigation.toEntries +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.bookmarksEntry +import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey +import com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation.forYouEntry +import com.google.samples.apps.nowinandroid.feature.interests.impl.navigation.interestsEntry +import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchNavKey +import com.google.samples.apps.nowinandroid.feature.search.impl.navigation.searchEntry +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsDialog +import com.google.samples.apps.nowinandroid.feature.topic.impl.navigation.topicEntry +import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS +import com.google.samples.apps.nowinandroid.feature.settings.impl.R as settingsR @Composable fun NiaApp( @@ -83,8 +92,7 @@ fun NiaApp( modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { - val shouldShowGradientBackground = - appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU + val shouldShowGradientBackground = appState.navigationState.currentTopLevelKey == ForYouNavKey var showSettingsDialog by rememberSaveable { mutableStateOf(false) } NiaBackground(modifier = modifier) { @@ -109,15 +117,17 @@ fun NiaApp( ) } } + CompositionLocalProvider(LocalSnackbarHostState provides snackbarHostState) { + NiaApp( + appState = appState, - NiaApp( - appState = appState, - snackbarHostState = snackbarHostState, - showSettingsDialog = showSettingsDialog, - onSettingsDismissed = { showSettingsDialog = false }, - onTopAppBarActionClick = { showSettingsDialog = true }, - windowAdaptiveInfo = windowAdaptiveInfo, - ) + // TODO: Settings should be a dialog screen + showSettingsDialog = showSettingsDialog, + onSettingsDismissed = { showSettingsDialog = false }, + onTopAppBarActionClick = { showSettingsDialog = true }, + windowAdaptiveInfo = windowAdaptiveInfo, + ) + } } } } @@ -126,19 +136,18 @@ fun NiaApp( @OptIn( ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class, + ExperimentalMaterial3AdaptiveApi::class, ) internal fun NiaApp( appState: NiaAppState, - snackbarHostState: SnackbarHostState, showSettingsDialog: Boolean, onSettingsDismissed: () -> Unit, onTopAppBarActionClick: () -> Unit, modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { - val unreadDestinations by appState.topLevelDestinationsWithUnreadResources + val unreadNavKeys by appState.topLevelNavKeysWithUnreadResources .collectAsStateWithLifecycle() - val currentDestination = appState.currentDestination if (showSettingsDialog) { SettingsDialog( @@ -146,28 +155,31 @@ internal fun NiaApp( ) } + val snackbarHostState = LocalSnackbarHostState.current + + val navigator = remember { Navigator(appState.navigationState) } + NiaNavigationSuiteScaffold( navigationSuiteItems = { - appState.topLevelDestinations.forEach { destination -> - val hasUnread = unreadDestinations.contains(destination) - val selected = currentDestination - .isRouteInHierarchy(destination.baseRoute) + TOP_LEVEL_NAV_ITEMS.forEach { (navKey, navItem) -> + val hasUnread = unreadNavKeys.contains(navKey) + val selected = navKey == appState.navigationState.currentTopLevelKey item( selected = selected, - onClick = { appState.navigateToTopLevelDestination(destination) }, + onClick = { navigator.navigate(navKey) }, icon = { Icon( - imageVector = destination.unselectedIcon, + imageVector = navItem.unselectedIcon, contentDescription = null, ) }, selectedIcon = { Icon( - imageVector = destination.selectedIcon, + imageVector = navItem.selectedIcon, contentDescription = null, ) }, - label = { Text(stringResource(destination.iconTextId)) }, + label = { Text(stringResource(navItem.iconTextId)) }, modifier = Modifier .testTag("NiaNavItem") .then(if (hasUnread) Modifier.notificationDot() else Modifier), @@ -205,27 +217,30 @@ internal fun NiaApp( ), ), ) { - // Show the top app bar on top level destinations. - val destination = appState.currentTopLevelDestination + // Only show the top app bar on top level destinations. var shouldShowTopAppBar = false - if (destination != null) { + if (appState.navigationState.currentKey in appState.navigationState.topLevelKeys) { shouldShowTopAppBar = true + + val destination = TOP_LEVEL_NAV_ITEMS[appState.navigationState.currentTopLevelKey] + ?: error("Top level nav item not found for ${appState.navigationState.currentTopLevelKey}") + NiaTopAppBar( titleRes = destination.titleTextId, navigationIcon = NiaIcons.Search, navigationIconContentDescription = stringResource( - id = settingsR.string.feature_settings_top_app_bar_navigation_icon_description, + id = settingsR.string.feature_settings_impl_top_app_bar_navigation_icon_description, ), actionIcon = NiaIcons.Settings, actionIconContentDescription = stringResource( - id = settingsR.string.feature_settings_top_app_bar_action_icon_description, + id = settingsR.string.feature_settings_impl_top_app_bar_action_icon_description, ), colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent, ), onActionClick = { onTopAppBarActionClick() }, - onNavigationClick = { appState.navigateToSearch() }, + onNavigationClick = { navigator.navigate(SearchNavKey) }, ) } @@ -239,15 +254,20 @@ internal fun NiaApp( }, ), ) { - NiaNavHost( - appState = appState, - onShowSnackbar = { message, action -> - snackbarHostState.showSnackbar( - message = message, - actionLabel = action, - duration = Short, - ) == ActionPerformed - }, + val listDetailStrategy = rememberListDetailSceneStrategy() + + val entryProvider = entryProvider { + forYouEntry(navigator) + bookmarksEntry(navigator) + interestsEntry(navigator) + topicEntry(navigator) + searchEntry(navigator) + } + + NavDisplay( + entries = appState.navigationState.toEntries(entryProvider), + sceneStrategy = listDetailStrategy, + onBack = { navigator.goBack() }, ) } @@ -276,8 +296,3 @@ private fun Modifier.notificationDot(): Modifier = ) } } - -private fun NavDestination?.isRouteInHierarchy(route: KClass<*>) = - this?.hierarchy?.any { - it.hasRoute(route) - } ?: false diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt index 7c892c854..9b4814980 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt @@ -18,30 +18,18 @@ package com.google.samples.apps.nowinandroid.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.navigation.NavController -import androidx.navigation.NavDestination -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navOptions -import androidx.tracing.trace +import androidx.navigation3.runtime.NavKey import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor +import com.google.samples.apps.nowinandroid.core.navigation.NavigationState +import com.google.samples.apps.nowinandroid.core.navigation.rememberNavigationState import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank -import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou -import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests -import com.google.samples.apps.nowinandroid.feature.search.navigation.navigateToSearch -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.BOOKMARKS -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU -import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey +import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey +import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -56,18 +44,20 @@ fun rememberNiaAppState( userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, coroutineScope: CoroutineScope = rememberCoroutineScope(), - navController: NavHostController = rememberNavController(), ): NiaAppState { - NavigationTrackingSideEffect(navController) + val navigationState = rememberNavigationState(ForYouNavKey, TOP_LEVEL_NAV_ITEMS.keys) + + NavigationTrackingSideEffect(navigationState) + return remember( - navController, + navigationState, coroutineScope, networkMonitor, userNewsResourceRepository, timeZoneMonitor, ) { NiaAppState( - navController = navController, + navigationState = navigationState, coroutineScope = coroutineScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, @@ -78,35 +68,12 @@ fun rememberNiaAppState( @Stable class NiaAppState( - val navController: NavHostController, + val navigationState: NavigationState, coroutineScope: CoroutineScope, networkMonitor: NetworkMonitor, userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, ) { - private val previousDestination = mutableStateOf(null) - - val currentDestination: NavDestination? - @Composable get() { - // Collect the currentBackStackEntryFlow as a state - val currentEntry = navController.currentBackStackEntryFlow - .collectAsState(initial = null) - - // Fallback to previousDestination if currentEntry is null - return currentEntry.value?.destination.also { destination -> - if (destination != null) { - previousDestination.value = destination - } - } ?: previousDestination.value - } - - val currentTopLevelDestination: TopLevelDestination? - @Composable get() { - return TopLevelDestination.entries.firstOrNull { topLevelDestination -> - currentDestination?.hasRoute(route = topLevelDestination.route) == true - } - } - val isOffline = networkMonitor.isOnline .map(Boolean::not) .stateIn( @@ -116,20 +83,14 @@ class NiaAppState( ) /** - * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the - * route. - */ - val topLevelDestinations: List = TopLevelDestination.entries - - /** - * The top level destinations that have unread news resources. + * The top level nav keys that have unread news resources. */ - val topLevelDestinationsWithUnreadResources: StateFlow> = + val topLevelNavKeysWithUnreadResources: StateFlow> = userNewsResourceRepository.observeAllForFollowedTopics() .combine(userNewsResourceRepository.observeAllBookmarked()) { forYouNewsResources, bookmarkedNewsResources -> setOfNotNull( - FOR_YOU.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, - BOOKMARKS.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, + ForYouNavKey.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, + BookmarksNavKey.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, ) } .stateIn( @@ -144,55 +105,15 @@ class NiaAppState( SharingStarted.WhileSubscribed(5_000), TimeZone.currentSystemDefault(), ) - - /** - * UI logic for navigating to a top level destination in the app. Top level destinations have - * only one copy of the destination of the back stack, and save and restore state whenever you - * navigate to and from it. - * - * @param topLevelDestination: The destination the app needs to navigate to. - */ - fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) { - trace("Navigation: ${topLevelDestination.name}") { - val topLevelNavOptions = navOptions { - // Pop up to the start destination of the graph to - // avoid building up a large stack of destinations - // on the back stack as users select items - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - // Avoid multiple copies of the same destination when - // reselecting the same item - launchSingleTop = true - // Restore state when reselecting a previously selected item - restoreState = true - } - - when (topLevelDestination) { - FOR_YOU -> navController.navigateToForYou(topLevelNavOptions) - BOOKMARKS -> navController.navigateToBookmarks(topLevelNavOptions) - INTERESTS -> navController.navigateToInterests(null, topLevelNavOptions) - } - } - } - - fun navigateToSearch() = navController.navigateToSearch() } /** * Stores information about navigation events to be used with JankStats */ @Composable -private fun NavigationTrackingSideEffect(navController: NavHostController) { - TrackDisposableJank(navController) { metricsHolder -> - val listener = NavController.OnDestinationChangedListener { _, destination, _ -> - metricsHolder.state?.putState("Navigation", destination.route.toString()) - } - - navController.addOnDestinationChangedListener(listener) - - onDispose { - navController.removeOnDestinationChangedListener(listener) - } +private fun NavigationTrackingSideEffect(navigationState: NavigationState) { + TrackDisposableJank(navigationState.currentKey) { metricsHolder -> + metricsHolder.state?.putState("Navigation", navigationState.currentKey.toString()) + onDispose {} } } diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt deleted file mode 100644 index 3d37f3417..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.samples.apps.nowinandroid.ui.interests2pane - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.navigation.toRoute -import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject - -const val TOPIC_ID_KEY = "selectedTopicId" - -@HiltViewModel -class Interests2PaneViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, -) : ViewModel() { - - val route = savedStateHandle.toRoute() - val selectedTopicId: StateFlow = savedStateHandle.getStateFlow( - key = TOPIC_ID_KEY, - initialValue = route.initialTopicId, - ) - - fun onTopicClick(topicId: String?) { - savedStateHandle[TOPIC_ID_KEY] = topicId - } -} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt deleted file mode 100644 index c0f425c65..000000000 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.samples.apps.nowinandroid.ui.interests2pane - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.LocalMinimumInteractiveComponentSize -import androidx.compose.material3.VerticalDragHandle -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.WindowAdaptiveInfo -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole -import androidx.compose.material3.adaptive.layout.PaneAdaptedValue -import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor -import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem -import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective -import androidx.compose.material3.adaptive.layout.defaultDragHandleSemantics -import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState -import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior -import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold -import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator -import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldPredictiveBackHandler -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.layout.layout -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute -import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute -import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder -import com.google.samples.apps.nowinandroid.feature.topic.TopicScreen -import com.google.samples.apps.nowinandroid.feature.topic.TopicViewModel -import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -import kotlin.math.max - -@Serializable internal object TopicPlaceholderRoute - -fun NavGraphBuilder.interestsListDetailScreen() { - composable { - InterestsListDetailScreen() - } -} - -@Composable -internal fun InterestsListDetailScreen( - viewModel: Interests2PaneViewModel = hiltViewModel(), - windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), -) { - val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle() - InterestsListDetailScreen( - selectedTopicId = selectedTopicId, - onTopicClick = viewModel::onTopicClick, - windowAdaptiveInfo = windowAdaptiveInfo, - ) -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -@Composable -internal fun InterestsListDetailScreen( - selectedTopicId: String?, - onTopicClick: (String) -> Unit, - windowAdaptiveInfo: WindowAdaptiveInfo, -) { - val listDetailNavigator = rememberListDetailPaneScaffoldNavigator( - scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo), - initialDestinationHistory = listOfNotNull( - ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List), - ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.Detail).takeIf { - selectedTopicId != null - }, - ), - ) - val coroutineScope = rememberCoroutineScope() - - val paneExpansionState = rememberPaneExpansionState( - anchors = listOf( - PaneExpansionAnchor.Proportion(0f), - PaneExpansionAnchor.Proportion(0.5f), - PaneExpansionAnchor.Proportion(1f), - ), - ) - - ThreePaneScaffoldPredictiveBackHandler( - listDetailNavigator, - BackNavigationBehavior.PopUntilScaffoldValueChange, - ) - BackHandler( - paneExpansionState.currentAnchor == PaneExpansionAnchor.Proportion(0f) && - listDetailNavigator.isListPaneVisible() && - listDetailNavigator.isDetailPaneVisible(), - ) { - coroutineScope.launch { - paneExpansionState.animateTo(PaneExpansionAnchor.Proportion(1f)) - } - } - - var topicRoute by remember { - val route = selectedTopicId?.let { TopicRoute(id = it) } ?: TopicPlaceholderRoute - mutableStateOf(route) - } - - fun onTopicClickShowDetailPane(topicId: String) { - onTopicClick(topicId) - topicRoute = TopicRoute(id = topicId) - coroutineScope.launch { - listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail) - } - if (paneExpansionState.currentAnchor == PaneExpansionAnchor.Proportion(1f)) { - coroutineScope.launch { - paneExpansionState.animateTo(PaneExpansionAnchor.Proportion(0f)) - } - } - } - - val mutableInteractionSource = remember { MutableInteractionSource() } - val minPaneWidth = 300.dp - - NavigableListDetailPaneScaffold( - navigator = listDetailNavigator, - listPane = { - AnimatedPane { - Box( - modifier = Modifier.clipToBounds() - .layout { measurable, constraints -> - val width = max(minPaneWidth.roundToPx(), constraints.maxWidth) - val placeable = measurable.measure( - constraints.copy( - minWidth = minPaneWidth.roundToPx(), - maxWidth = width, - ), - ) - layout(constraints.maxWidth, placeable.height) { - placeable.placeRelative( - x = 0, - y = 0, - ) - } - }, - ) { - InterestsRoute( - onTopicClick = ::onTopicClickShowDetailPane, - shouldHighlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(), - ) - } - } - }, - detailPane = { - AnimatedPane { - Box( - modifier = Modifier.clipToBounds() - .layout { measurable, constraints -> - val width = max(minPaneWidth.roundToPx(), constraints.maxWidth) - val placeable = measurable.measure( - constraints.copy( - minWidth = minPaneWidth.roundToPx(), - maxWidth = width, - ), - ) - layout(constraints.maxWidth, placeable.height) { - placeable.placeRelative( - x = constraints.maxWidth - - max(constraints.maxWidth, placeable.width), - y = 0, - ) - } - }, - ) { - AnimatedContent(topicRoute) { route -> - when (route) { - is TopicRoute -> { - TopicScreen( - showBackButton = !listDetailNavigator.isListPaneVisible(), - onBackClick = { - coroutineScope.launch { - listDetailNavigator.navigateBack() - } - }, - onTopicClick = ::onTopicClickShowDetailPane, - viewModel = hiltViewModel( - key = route.id, - ) { factory -> - factory.create(route.id) - }, - ) - } - is TopicPlaceholderRoute -> { - TopicDetailPlaceholder() - } - } - } - } - } - }, - paneExpansionState = paneExpansionState, - paneExpansionDragHandle = { - VerticalDragHandle( - modifier = Modifier.paneExpansionDraggable( - state = paneExpansionState, - minTouchTargetSize = LocalMinimumInteractiveComponentSize.current, - interactionSource = mutableInteractionSource, - semanticsProperties = paneExpansionState.defaultDragHandleSemantics(), - ), - interactionSource = mutableInteractionSource, - ) - }, - ) -} - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -private fun ThreePaneScaffoldNavigator.isListPaneVisible(): Boolean = - scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded - -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -private fun ThreePaneScaffoldNavigator.isDetailPaneVisible(): Boolean = - scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index c6ddb54fb..810b77cf0 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -16,21 +16,19 @@ package com.google.samples.apps.nowinandroid.ui -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.junit4.createComposeRule -import androidx.navigation.NavHostController -import androidx.navigation.compose.ComposeNavigator -import androidx.navigation.compose.composable -import androidx.navigation.createGraph -import androidx.navigation.testing.TestNavHostController +import androidx.navigation3.runtime.NavBackStack import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository +import com.google.samples.apps.nowinandroid.core.navigation.NavigationState +import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey +import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication import kotlinx.coroutines.flow.collect @@ -44,7 +42,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import kotlin.test.assertEquals -import kotlin.test.assertTrue /** * Tests [NiaAppState]. @@ -68,32 +65,42 @@ class NiaAppStateTest { // Subject under test. private lateinit var state: NiaAppState + private fun testNavigationState() = NavigationState( + startKey = ForYouNavKey, + topLevelStack = NavBackStack(ForYouNavKey), + subStacks = mapOf( + ForYouNavKey to NavBackStack(ForYouNavKey), + BookmarksNavKey to NavBackStack(BookmarksNavKey), + ), + ) + @Test fun niaAppState_currentDestination() = runTest { - var currentDestination: String? = null + val navigationState = testNavigationState() + val navigator = Navigator(navigationState) composeTestRule.setContent { - val navController = rememberTestNavController() - state = remember(navController) { + state = remember(navigationState) { NiaAppState( - navController = navController, coroutineScope = backgroundScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, + navigationState = navigationState, ) } + } - // Update currentDestination whenever it changes - currentDestination = state.currentDestination?.route + assertEquals(ForYouNavKey, state.navigationState.currentTopLevelKey) + assertEquals(ForYouNavKey, state.navigationState.currentKey) - // Navigate to destination b once - LaunchedEffect(Unit) { - navController.setCurrentDestination("b") - } - } + // Navigate to another destination once + navigator.navigate(BookmarksNavKey) - assertEquals("b", currentDestination) + composeTestRule.waitForIdle() + + assertEquals(BookmarksNavKey, state.navigationState.currentTopLevelKey) + assertEquals(BookmarksNavKey, state.navigationState.currentKey) } @Test @@ -106,21 +113,24 @@ class NiaAppStateTest { ) } - assertEquals(3, state.topLevelDestinations.size) - assertTrue(state.topLevelDestinations[0].name.contains("for_you", true)) - assertTrue(state.topLevelDestinations[1].name.contains("bookmarks", true)) - assertTrue(state.topLevelDestinations[2].name.contains("interests", true)) + val navigationState = state.navigationState + + assertEquals(3, navigationState.topLevelKeys.size) + assertEquals( + setOf(ForYouNavKey, BookmarksNavKey, InterestsNavKey(null)), + navigationState.topLevelKeys, + ) } @Test fun niaAppState_whenNetworkMonitorIsOffline_StateIsOffline() = runTest(UnconfinedTestDispatcher()) { composeTestRule.setContent { state = NiaAppState( - navController = NavHostController(LocalContext.current), coroutineScope = backgroundScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, + navigationState = testNavigationState(), ) } @@ -136,11 +146,11 @@ class NiaAppStateTest { fun niaAppState_differentTZ_withTimeZoneMonitorChange() = runTest(UnconfinedTestDispatcher()) { composeTestRule.setContent { state = NiaAppState( - navController = NavHostController(LocalContext.current), coroutineScope = backgroundScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, + navigationState = testNavigationState(), ) } val changedTz = TimeZone.of("Europe/Prague") @@ -152,18 +162,3 @@ class NiaAppStateTest { ) } } - -@Composable -private fun rememberTestNavController(): TestNavHostController { - val context = LocalContext.current - return remember { - TestNavHostController(context).apply { - navigatorProvider.addNavigator(ComposeNavigator()) - graph = createGraph(startDestination = "a") { - composable("a") { } - composable("b") { } - composable("c") { } - } - } - } -} diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt index 78f568e03..3c7610193 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt @@ -67,6 +67,7 @@ import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -147,9 +148,7 @@ class SnackbarInsetsScreenshotTests { @Test fun phone_noSnackbar() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 400.dp, 500.dp, "insets_snackbar_compact_medium_noSnackbar", @@ -159,13 +158,11 @@ class SnackbarInsetsScreenshotTests { @Test fun snackbarShown_phone() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 400.dp, 500.dp, "insets_snackbar_compact_medium", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -176,13 +173,11 @@ class SnackbarInsetsScreenshotTests { @Test fun snackbarShown_foldable() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 600.dp, 600.dp, "insets_snackbar_medium_medium", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -193,13 +188,11 @@ class SnackbarInsetsScreenshotTests { @Test fun snackbarShown_tablet() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 900.dp, 900.dp, "insets_snackbar_expanded_expanded", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -209,17 +202,18 @@ class SnackbarInsetsScreenshotTests { } private fun testSnackbarScreenshotWithSize( - snackbarHostState: SnackbarHostState, width: Dp, height: Dp, screenshotName: String, - action: suspend () -> Unit, + action: suspend (snackbarHostState: SnackbarHostState) -> Unit, ) { lateinit var scope: CoroutineScope + val snackbarHostState = SnackbarHostState() composeTestRule.setContent { CompositionLocalProvider( // Replaces images with placeholders LocalInspectionMode provides true, + LocalSnackbarHostState provides snackbarHostState, ) { scope = rememberCoroutineScope() @@ -259,7 +253,6 @@ class SnackbarInsetsScreenshotTests { ) NiaApp( appState = appState, - snackbarHostState = snackbarHostState, showSettingsDialog = false, onSettingsDismissed = {}, onTopAppBarActionClick = {}, @@ -280,7 +273,7 @@ class SnackbarInsetsScreenshotTests { } scope.launch { - action() + action(snackbarHostState) } composeTestRule.onNodeWithTag("root") diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt index b9b1047c1..75dc6baa7 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt @@ -40,6 +40,7 @@ import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -120,9 +121,7 @@ class SnackbarScreenshotTests { @Test fun phone_noSnackbar() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 400.dp, 500.dp, "snackbar_compact_medium_noSnackbar", @@ -132,13 +131,11 @@ class SnackbarScreenshotTests { @Test fun snackbarShown_phone() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 400.dp, 500.dp, "snackbar_compact_medium", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -149,13 +146,11 @@ class SnackbarScreenshotTests { @Test fun snackbarShown_foldable() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 600.dp, 600.dp, "snackbar_medium_medium", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -166,13 +161,11 @@ class SnackbarScreenshotTests { @Test fun snackbarShown_tablet() { - val snackbarHostState = SnackbarHostState() testSnackbarScreenshotWithSize( - snackbarHostState, 900.dp, 900.dp, "snackbar_expanded_expanded", - ) { + ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", @@ -182,17 +175,19 @@ class SnackbarScreenshotTests { } private fun testSnackbarScreenshotWithSize( - snackbarHostState: SnackbarHostState, width: Dp, height: Dp, screenshotName: String, - action: suspend () -> Unit, + action: suspend (snackbarHostState: SnackbarHostState) -> Unit, ) { lateinit var scope: CoroutineScope + val snackbarHostState = SnackbarHostState() composeTestRule.setContent { CompositionLocalProvider( // Replaces images with placeholders LocalInspectionMode provides true, + LocalSnackbarHostState provides snackbarHostState, + ) { scope = rememberCoroutineScope() @@ -208,7 +203,6 @@ class SnackbarScreenshotTests { ) NiaApp( appState = appState, - snackbarHostState = snackbarHostState, showSettingsDialog = false, onSettingsDismissed = {}, onTopAppBarActionClick = {}, @@ -227,7 +221,7 @@ class SnackbarScreenshotTests { } scope.launch { - action() + action(snackbarHostState) } composeTestRule.onRoot() diff --git a/app/src/testDemo/resources/robolectric.properties b/app/src/testDemo/resources/robolectric.properties new file mode 100644 index 000000000..ca82be153 --- /dev/null +++ b/app/src/testDemo/resources/robolectric.properties @@ -0,0 +1,17 @@ +# +# Copyright 2025 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +sdk = 35 \ No newline at end of file diff --git a/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png index 382eb032b..a001de939 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png index cde193ce9..d13212045 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png index 4a8c93d96..309fd305f 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png index f0a85e137..78d792c9e 100644 Binary files a/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png index 222fc21aa..77dc72def 100644 Binary files a/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png index a33ad8863..84e59fabe 100644 Binary files a/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png b/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png index db6488e34..cfbbc455e 100644 Binary files a/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png and b/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png index f5d5c2d18..c2a85bf1f 100644 Binary files a/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png and b/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png b/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png index 52a32c385..97bd7919b 100644 Binary files a/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png and b/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png index deeede5e4..74f8d2862 100644 Binary files a/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png index 882e62760..e5f3f47ec 100644 Binary files a/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png index 8ffe73e86..77ed5f958 100644 Binary files a/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/snackbar_compact_medium.png b/app/src/testDemo/screenshots/snackbar_compact_medium.png index 910cccfc2..0e8da24ca 100644 Binary files a/app/src/testDemo/screenshots/snackbar_compact_medium.png and b/app/src/testDemo/screenshots/snackbar_compact_medium.png differ diff --git a/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png b/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png index 8ad664d3a..317797542 100644 Binary files a/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png and b/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png differ diff --git a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png index b429a7315..f324fb9fe 100644 Binary files a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png and b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png differ diff --git a/app/src/testDemo/screenshots/snackbar_medium_medium.png b/app/src/testDemo/screenshots/snackbar_medium_medium.png index 00e9e3051..7921bbbbf 100644 Binary files a/app/src/testDemo/screenshots/snackbar_medium_medium.png and b/app/src/testDemo/screenshots/snackbar_medium_medium.png differ diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 000000000..c2bbf2a2a --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,178 @@ +# `:benchmarks` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:settings + direction TB + :feature:settings:impl[impl]:::android-library + end + subgraph :feature:foryou + direction TB + :feature:foryou:api[api]:::android-library + :feature:foryou:impl[impl]:::android-library + end + subgraph :feature:bookmarks + direction TB + :feature:bookmarks:api[api]:::android-library + :feature:bookmarks:impl[impl]:::android-library + end + subgraph :feature:search + direction TB + :feature:search:api[api]:::android-library + :feature:search:impl[impl]:::android-library + end + subgraph :feature:interests + direction TB + :feature:interests:api[api]:::android-library + :feature:interests:impl[impl]:::android-library + end + subgraph :feature:topic + direction TB + :feature:topic:api[api]:::android-library + :feature:topic:impl[impl]:::android-library + end + end + subgraph :sync + direction TB + :sync:work[work]:::android-library + end + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:designsystem[designsystem]:::android-library + :core:domain[domain]:::android-library + :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + :core:ui[ui]:::android-library + end + :benchmarks[benchmarks]:::android-test + :app[app]:::android-application + + :app -.->|baselineProfile| :benchmarks + :app -.-> :core:analytics + :app -.-> :core:common + :app -.-> :core:data + :app -.-> :core:designsystem + :app -.-> :core:model + :app -.-> :core:ui + :app -.-> :feature:bookmarks:api + :app -.-> :feature:bookmarks:impl + :app -.-> :feature:foryou:api + :app -.-> :feature:foryou:impl + :app -.-> :feature:interests:api + :app -.-> :feature:interests:impl + :app -.-> :feature:search:api + :app -.-> :feature:search:impl + :app -.-> :feature:settings:impl + :app -.-> :feature:topic:api + :app -.-> :feature:topic:impl + :app -.-> :sync:work + :benchmarks -.->|testedApks| :app + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:domain --> :core:data + :core:domain --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :core:model + :core:ui --> :core:analytics + :core:ui --> :core:designsystem + :core:ui --> :core:model + :feature:bookmarks:api --> :core:navigation + :feature:bookmarks:impl -.-> :core:data + :feature:bookmarks:impl -.-> :core:designsystem + :feature:bookmarks:impl -.-> :core:ui + :feature:bookmarks:impl -.-> :feature:bookmarks:api + :feature:bookmarks:impl -.-> :feature:topic:api + :feature:foryou:api --> :core:navigation + :feature:foryou:impl -.-> :core:designsystem + :feature:foryou:impl -.-> :core:domain + :feature:foryou:impl -.-> :core:notifications + :feature:foryou:impl -.-> :core:ui + :feature:foryou:impl -.-> :feature:foryou:api + :feature:foryou:impl -.-> :feature:topic:api + :feature:interests:api --> :core:navigation + :feature:interests:impl -.-> :core:designsystem + :feature:interests:impl -.-> :core:domain + :feature:interests:impl -.-> :core:ui + :feature:interests:impl -.-> :feature:interests:api + :feature:interests:impl -.-> :feature:topic:api + :feature:search:api -.-> :core:domain + :feature:search:api --> :core:navigation + :feature:search:impl -.-> :core:designsystem + :feature:search:impl -.-> :core:domain + :feature:search:impl -.-> :core:ui + :feature:search:impl -.-> :feature:interests:api + :feature:search:impl -.-> :feature:search:api + :feature:search:impl -.-> :feature:topic:api + :feature:settings:impl -.-> :core:data + :feature:settings:impl -.-> :core:designsystem + :feature:settings:impl -.-> :core:ui + :feature:topic:api -.-> :core:designsystem + :feature:topic:api --> :core:navigation + :feature:topic:api -.-> :core:ui + :feature:topic:impl -.-> :core:data + :feature:topic:impl -.-> :core:designsystem + :feature:topic:impl -.-> :core:ui + :feature:topic:impl -.-> :feature:topic:api + :sync:work -.-> :core:analytics + :sync:work -.-> :core:data + :sync:work -.-> :core:notifications + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index e36f021b0..258993913 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -45,8 +45,8 @@ android { ) } - testOptions.managedDevices.devices { - create("pixel6Api33") { + testOptions.managedDevices.localDevices { + create("pixel6Api33") { device = "Pixel 6" apiLevel = 33 systemImageSource = "aosp" diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 6d0237010..5faf257c1 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -78,9 +78,13 @@ gradlePlugin { id = libs.plugins.nowinandroid.android.library.asProvider().get().pluginId implementationClass = "AndroidLibraryConventionPlugin" } - register("androidFeature") { - id = libs.plugins.nowinandroid.android.feature.get().pluginId - implementationClass = "AndroidFeatureConventionPlugin" + register("androidFeatureImpl") { + id = libs.plugins.nowinandroid.android.feature.impl.get().pluginId + implementationClass = "AndroidFeatureImplConventionPlugin" + } + register("androidFeatureApi") { + id = libs.plugins.nowinandroid.android.feature.api.get().pluginId + implementationClass = "AndroidFeatureApiConventionPlugin" } register("androidLibraryJacoco") { id = libs.plugins.nowinandroid.android.library.jacoco.get().pluginId @@ -114,5 +118,9 @@ gradlePlugin { id = libs.plugins.nowinandroid.jvm.library.get().pluginId implementationClass = "JvmLibraryConventionPlugin" } + register("root") { + id = libs.plugins.nowinandroid.root.get().pluginId + implementationClass = "RootPlugin" + } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt index a8b1b1779..0a33b719f 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -31,5 +31,4 @@ class AndroidApplicationComposeConventionPlugin : Plugin { configureAndroidCompose(extension) } } - } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 1ab3a2ca0..8c45a056c 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -16,7 +16,6 @@ import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension -import com.android.build.gradle.BaseExtension import com.google.samples.apps.nowinandroid.configureBadgingTasks import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.configureKotlinAndroid @@ -25,26 +24,23 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure -import org.gradle.kotlin.dsl.getByType class AndroidApplicationConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { apply(plugin = "com.android.application") - apply(plugin = "org.jetbrains.kotlin.android") apply(plugin = "nowinandroid.android.lint") apply(plugin = "com.dropbox.dependency-guard") extensions.configure { configureKotlinAndroid(this) - defaultConfig.targetSdk = 35 - @Suppress("UnstableApiUsage") + defaultConfig.targetSdk = 36 testOptions.animationsDisabled = true configureGradleManagedDevices(this) } extensions.configure { configurePrintApksTask(this) - configureBadgingTasks(extensions.getByType(), this) + configureBadgingTasks(this) } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt index b0eece41d..326d03f4f 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt @@ -21,20 +21,16 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.getByType +import org.gradle.testing.jacoco.plugins.JacocoPlugin class AndroidApplicationJacocoConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - apply(plugin = "jacoco") - - val androidExtension = extensions.getByType() - - androidExtension.buildTypes.configureEach { - enableAndroidTestCoverage = true - enableUnitTestCoverage = true - } - - configureJacoco(extensions.getByType()) + apply() + configureJacoco( + commonExtension = extensions.getByType(), + androidComponentsExtension = extensions.getByType(), + ) } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureApiConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureApiConventionPlugin.kt new file mode 100644 index 000000000..969cf96d4 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureApiConventionPlugin.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.samples.apps.nowinandroid.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.dependencies + +class AndroidFeatureApiConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + apply(plugin = "nowinandroid.android.library") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") + + dependencies { + "api"(project(":core:navigation")) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureImplConventionPlugin.kt similarity index 83% rename from build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt rename to build-logic/convention/src/main/kotlin/AndroidFeatureImplConventionPlugin.kt index 1af5523c5..500e3e983 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureImplConventionPlugin.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.libs import org.gradle.api.Plugin @@ -23,12 +23,11 @@ import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies -class AndroidFeatureConventionPlugin : Plugin { +class AndroidFeatureImplConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { apply(plugin = "nowinandroid.android.library") apply(plugin = "nowinandroid.hilt") - apply(plugin = "org.jetbrains.kotlin.plugin.serialization") extensions.configure { testOptions.animationsDisabled = true @@ -39,14 +38,12 @@ class AndroidFeatureConventionPlugin : Plugin { "implementation"(project(":core:ui")) "implementation"(project(":core:designsystem")) - "implementation"(libs.findLibrary("androidx.hilt.navigation.compose").get()) "implementation"(libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) "implementation"(libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) - "implementation"(libs.findLibrary("androidx.navigation.compose").get()) + "implementation"(libs.findLibrary("androidx.hilt.lifecycle.viewModelCompose").get()) + "implementation"(libs.findLibrary("androidx.navigation3.runtime").get()) "implementation"(libs.findLibrary("androidx.tracing.ktx").get()) - "implementation"(libs.findLibrary("kotlinx.serialization.json").get()) - "testImplementation"(libs.findLibrary("androidx.navigation.testing").get()) "androidTestImplementation"( libs.findLibrary("androidx.lifecycle.runtimeTesting").get(), ) diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 19fabf549..18cd2bd7d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension import com.google.samples.apps.nowinandroid.configureAndroidCompose import org.gradle.api.Plugin import org.gradle.api.Project @@ -31,5 +31,4 @@ class AndroidLibraryComposeConventionPlugin : Plugin { configureAndroidCompose(extension) } } - } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 3fe727410..ee47e823a 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -14,8 +14,8 @@ * limitations under the License. */ +import com.android.build.api.dsl.LibraryExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension -import com.android.build.gradle.LibraryExtension import com.google.samples.apps.nowinandroid.configureFlavors import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.configureKotlinAndroid @@ -32,12 +32,12 @@ class AndroidLibraryConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { apply(plugin = "com.android.library") - apply(plugin = "org.jetbrains.kotlin.android") apply(plugin = "nowinandroid.android.lint") extensions.configure { configureKotlinAndroid(this) - defaultConfig.targetSdk = 35 + testOptions.targetSdk = 36 + lint.targetSdk = 36 defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testOptions.animationsDisabled = true configureFlavors(this) @@ -55,6 +55,7 @@ class AndroidLibraryConventionPlugin : Plugin { dependencies { "androidTestImplementation"(libs.findLibrary("kotlin.test").get()) "testImplementation"(libs.findLibrary("kotlin.test").get()) + "testImplementation"(libs.findLibrary("junit").get()) "implementation"(libs.findLibrary("androidx.tracing.ktx").get()) } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt index d249e4cbf..2fd97d456 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt @@ -21,20 +21,16 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.getByType +import org.gradle.testing.jacoco.plugins.JacocoPlugin class AndroidLibraryJacocoConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - apply(plugin = "jacoco") - - val androidExtension = extensions.getByType() - - androidExtension.buildTypes.configureEach { - enableAndroidTestCoverage = true - enableUnitTestCoverage = true - } - - configureJacoco(extensions.getByType()) + apply() + configureJacoco( + commonExtension = extensions.getByType(), + androidComponentsExtension = extensions.getByType(), + ) } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt index 67933f77d..8a2f0b5a4 100644 --- a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -import com.android.build.gradle.TestExtension +import com.android.build.api.dsl.TestExtension import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.configureKotlinAndroid import org.gradle.api.Plugin @@ -26,11 +26,10 @@ class AndroidTestConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { apply(plugin = "com.android.test") - apply(plugin = "org.jetbrains.kotlin.android") extensions.configure { configureKotlinAndroid(this) - defaultConfig.targetSdk = 35 + defaultConfig.targetSdk = 36 configureGradleManagedDevices(this) } } diff --git a/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt index 5a90ff98f..82bf9f60c 100644 --- a/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt @@ -28,6 +28,7 @@ class HiltConventionPlugin : Plugin { dependencies { "ksp"(libs.findLibrary("hilt.compiler").get()) + "ksp"(libs.findLibrary("kotlin.metadata").get()) } // Add support for Jvm Module, base on org.jetbrains.kotlin.jvm diff --git a/build-logic/convention/src/main/kotlin/RootPlugin.kt b/build-logic/convention/src/main/kotlin/RootPlugin.kt new file mode 100644 index 000000000..b704adf76 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/RootPlugin.kt @@ -0,0 +1,26 @@ +/* + * 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. + */ + +import com.google.samples.apps.nowinandroid.configureGraphTasks +import org.gradle.api.Plugin +import org.gradle.api.Project + +class RootPlugin : Plugin { + override fun apply(target: Project) { + require(target.path == ":") + target.subprojects { configureGraphTasks() } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt index ed2a5289b..ca2a6c99b 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt @@ -19,7 +19,6 @@ package com.google.samples.apps.nowinandroid import com.android.build.api.dsl.CommonExtension import org.gradle.api.Project import org.gradle.api.provider.Provider -import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension @@ -28,10 +27,10 @@ import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginE * Configure Compose-specific options */ internal fun Project.configureAndroidCompose( - commonExtension: CommonExtension<*, *, *, *, *, *>, + commonExtension: CommonExtension, ) { commonExtension.apply { - buildFeatures { + buildFeatures.apply { compose = true } @@ -42,18 +41,12 @@ internal fun Project.configureAndroidCompose( "implementation"(libs.findLibrary("androidx-compose-ui-tooling-preview").get()) "debugImplementation"(libs.findLibrary("androidx-compose-ui-tooling").get()) } - - testOptions { - unitTests { - // For Robolectric - isIncludeAndroidResources = true - } - } } extensions.configure { fun Provider.onlyIfTrue() = flatMap { provider { it.takeIf(String::toBoolean) } } fun Provider<*>.relativeToRootProject(dir: String) = map { + @Suppress("UnstableApiUsage") isolated.rootProject.projectDirectory .dir("build") .dir(projectDir.toRelativeString(rootDir)) @@ -67,6 +60,7 @@ internal fun Project.configureAndroidCompose( .relativeToRootProject("compose-reports") .let(reportsDestination::set) + @Suppress("UnstableApiUsage") stabilityConfigurationFiles .add(isolated.rootProject.projectDirectory.file("compose_compiler_config.conf")) } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt index 886c70625..e747325ee 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt @@ -16,10 +16,9 @@ package com.google.samples.apps.nowinandroid -import com.android.SdkConstants import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.Aapt2 import com.android.build.api.variant.ApplicationAndroidComponentsExtension -import com.android.build.gradle.BaseExtension import com.google.common.truth.Truth.assertWithMessage import org.gradle.api.DefaultTask import org.gradle.api.Project @@ -110,7 +109,6 @@ private fun String.capitalized() = replaceFirstChar { } fun Project.configureBadgingTasks( - baseExtension: BaseExtension, componentsExtension: ApplicationAndroidComponentsExtension, ) { // Registers a callback to be called, when a new variant is configured @@ -121,17 +119,7 @@ fun Project.configureBadgingTasks( val generateBadging = tasks.register(generateBadgingTaskName) { apk = variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE) - aapt2Executable.set( - // TODO: Replace with `sdkComponents.aapt2` when it's available in AGP - // https://issuetracker.google.com/issues/376815836 - componentsExtension.sdkComponents.sdkDirectory.map { directory -> - directory.file( - "${SdkConstants.FD_BUILD_TOOLS}/" + - "${baseExtension.buildToolsVersion}/" + - SdkConstants.FN_AAPT2, - ) - } - ) + aapt2Executable = componentsExtension.sdkComponents.aapt2.flatMap(Aapt2::executable) badging = project.layout.buildDirectory.file( "outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt", ) diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt index f67e9093d..992a3f91e 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt @@ -25,7 +25,7 @@ import org.gradle.kotlin.dsl.invoke * Configure project for Gradle managed devices */ internal fun configureGradleManagedDevices( - commonExtension: CommonExtension<*, *, *, *, *, *>, + commonExtension: CommonExtension, ) { val pixel4 = DeviceConfig("Pixel 4", 30, "aosp-atd") val pixel6 = DeviceConfig("Pixel 6", 31, "aosp") @@ -34,9 +34,11 @@ internal fun configureGradleManagedDevices( val allDevices = listOf(pixel4, pixel6, pixelC) val ciDevices = listOf(pixel4, pixelC) - commonExtension.testOptions { + + commonExtension.testOptions.apply { + @Suppress("UnstableApiUsage") managedDevices { - devices { + allDevices { allDevices.forEach { deviceConfig -> maybeCreate(deviceConfig.taskName, ManagedVirtualDevice::class.java).apply { device = deviceConfig.device @@ -48,7 +50,7 @@ internal fun configureGradleManagedDevices( groups { maybeCreate("ci").apply { ciDevices.forEach { deviceConfig -> - targetDevices.add(devices[deviceConfig.taskName]) + targetDevices.add(localDevices[deviceConfig.taskName]) } } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Graph.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Graph.kt new file mode 100644 index 000000000..80448e2b2 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Graph.kt @@ -0,0 +1,353 @@ +/* + * 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 + +import com.android.utils.associateWithNotNull +import com.google.samples.apps.nowinandroid.PluginType.Unknown +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity.NONE +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.assign +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType +import kotlin.text.RegexOption.DOT_MATCHES_ALL + +/** + * Generates module dependency graphs with `graphDump` task, and update the corresponding `README.md` file with `graphUpdate`. + * + * This is not an optimal implementation and could be improved if needed: + * - [Graph.invoke] is **recursively** searching through dependent projects (although in practice it will never reach a stack overflow). + * - [Graph.invoke] is entirely re-executed for all projects, without re-using intermediate values. + * - [Graph.invoke] is always executed during Gradle's Configuration phase (but takes in general less than 1 ms for a project). + * + * The resulting graphs can be configured with `graph.ignoredProjects` and `graph.supportedConfigurations` properties. + */ +private class Graph( + private val root: Project, + private val dependencies: MutableMap>> = mutableMapOf(), + private val plugins: MutableMap = mutableMapOf(), + private val seen: MutableSet = mutableSetOf(), +) { + + private val ignoredProjects = root.providers.gradleProperty("graph.ignoredProjects") + .map { it.split(",").toSet() } + .orElse(emptySet()) + private val supportedConfigurations = + root.providers.gradleProperty("graph.supportedConfigurations") + .map { it.split(",").toSet() } + .orElse(setOf("api", "implementation", "baselineProfile", "testedApks")) + + operator fun invoke(project: Project = root): Graph { + if (project.path in seen) return this + seen += project.path + plugins.putIfAbsent( + project, + PluginType.entries.firstOrNull { project.pluginManager.hasPlugin(it.id) } ?: Unknown, + ) + dependencies.compute(project) { _, u -> u.orEmpty() } + project.configurations + .matching { it.name in supportedConfigurations.get() } + .associateWithNotNull { it.dependencies.withType().ifEmpty { null } } + .flatMap { (c, value) -> value.map { dep -> c to project.project(dep.path) } } + .filter { (_, p) -> p.path !in ignoredProjects.get() } + .forEach { (configuration: Configuration, projectDependency: Project) -> + dependencies.compute(project) { _, u -> u.orEmpty() + (configuration to projectDependency) } + invoke(projectDependency) + } + return this + } + + fun dependencies(): Map>> = dependencies + .mapKeys { it.key.path } + .mapValues { it.value.mapTo(mutableSetOf()) { (c, p) -> c.name to p.path } } + + fun plugins() = plugins.mapKeys { it.key.path } +} + +/** + * Declaration order is important, as only the first match will be retained. + */ +internal enum class PluginType(val id: String, val ref: String, val style: String) { + AndroidApplication( + id = "nowinandroid.android.application", + ref = "android-application", + style = "fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000", + ), + AndroidFeature( + id = "nowinandroid.android.feature", + ref = "android-feature", + style = "fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000", + ), + AndroidLibrary( + id = "nowinandroid.android.library", + ref = "android-library", + style = "fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000", + ), + AndroidTest( + id = "nowinandroid.android.test", + ref = "android-test", + style = "fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000", + ), + Jvm( + id = "nowinandroid.jvm.library", + ref = "jvm-library", + style = "fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000", + ), + Unknown( + id = "?", + ref = "unknown", + style = "fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000", + ), +} + +internal fun Project.configureGraphTasks() { + if (!buildFile.exists()) return // Ignore root modules without build file + val dumpTask = tasks.register("graphDump") { + val graph = Graph(this@configureGraphTasks).invoke() + projectPath = this@configureGraphTasks.path + dependencies = graph.dependencies() + plugins = graph.plugins() + output = this@configureGraphTasks.layout.buildDirectory.file("mermaid/graph.txt") + legend = this@configureGraphTasks.layout.buildDirectory.file("mermaid/legend.txt") + } + tasks.register("graphUpdate") { + projectPath = this@configureGraphTasks.path + input = dumpTask.flatMap { it.output } + legend = dumpTask.flatMap { it.legend } + output = this@configureGraphTasks.layout.projectDirectory.file("README.md") + } +} + +@CacheableTask +private abstract class GraphDumpTask : DefaultTask() { + + @get:Input + abstract val projectPath: Property + + @get:Input + abstract val dependencies: MapProperty>> + + @get:Input + abstract val plugins: MapProperty + + @get:OutputFile + abstract val output: RegularFileProperty + + @get:OutputFile + abstract val legend: RegularFileProperty + + override fun getDescription() = "Dumps project dependencies to a mermaid file." + + @TaskAction + operator fun invoke() { + output.get().asFile.writeText(mermaid()) + legend.get().asFile.writeText(legend()) + logger.lifecycle(output.get().asFile.toPath().toUri().toString()) + } + + private fun mermaid() = buildString { + val dependencies: Set = dependencies.get() + .flatMapTo(mutableSetOf()) { (project, entries) -> entries.map { it.toDependency(project) } } + // FrontMatter configuration (not supported yet on GitHub.com) + appendLine( + // language=YAML + """ + --- + config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE + --- + """.trimIndent(), + ) + // Graph declaration + appendLine("graph TB") + // Nodes and subgraphs + val (rootProjects, nestedProjects) = dependencies + .map { listOf(it.project, it.dependency) }.flatten().toSet() + .plus(projectPath.get()) // Special case when this specific module has no other dependency + .groupBy { it.substringBeforeLast(":") } + .entries.partition { it.key.isEmpty() } + + val orderedGroups = nestedProjects.groupBy { + if (it.key.count { char -> char == ':' } > 1) it.key.substringBeforeLast(":") else "" + } + + orderedGroups.forEach { (outerGroup, innerGroups) -> + if (outerGroup.isNotEmpty()) { + appendLine(" subgraph $outerGroup") + appendLine(" direction TB") + } + innerGroups.sortedWith( + compareBy( + { (group, _) -> + dependencies.filter { dep -> + val toGroup = dep.dependency.substringBeforeLast(":") + toGroup == group && dep.project.substringBeforeLast(":") != group + }.count() + }, + { -it.value.size }, + ), + ).forEach { (group, projects) -> + val indent = if (outerGroup.isNotEmpty()) 4 else 2 + appendLine(" ".repeat(indent) + "subgraph $group") + appendLine(" ".repeat(indent) + " direction TB") + projects.sorted().forEach { + appendLine(it.alias(indent = indent + 2, plugins.get().getValue(it))) + } + appendLine(" ".repeat(indent) + "end") + } + if (outerGroup.isNotEmpty()) { + appendLine(" end") + } + } + + rootProjects.flatMap { it.value }.sortedDescending().forEach { + appendLine(it.alias(indent = 2, plugins.get().getValue(it))) + } + // Links + if (dependencies.isNotEmpty()) appendLine() + dependencies + .sortedWith(compareBy({ it.project }, { it.dependency }, { it.configuration })) + .forEach { appendLine(it.link(indent = 2)) } + // Classes + appendLine() + PluginType.entries.forEach { appendLine(it.classDef()) } + } + + private fun legend() = buildString { + appendLine("graph TB") + listOf( + "application" to PluginType.AndroidApplication, + "feature" to PluginType.AndroidFeature, + "library" to PluginType.AndroidLibrary, + "jvm" to PluginType.Jvm, + ).forEach { (name, type) -> + appendLine(name.alias(indent = 2, type)) + } + appendLine() + listOf( + Dependency("application", "implementation", "feature"), + Dependency("library", "api", "jvm"), + ).forEach { + appendLine(it.link(indent = 2)) + } + appendLine() + PluginType.entries.forEach { appendLine(it.classDef()) } + } + + private class Dependency(val project: String, val configuration: String, val dependency: String) + + private fun Pair.toDependency(project: String) = + Dependency(project, configuration = first, dependency = second) + + private fun String.alias(indent: Int, pluginType: PluginType): String = buildString { + append(" ".repeat(indent)) + append(this@alias) + append("[").append(substringAfterLast(":")).append("]:::") + append(pluginType.ref) + } + + private fun Dependency.link(indent: Int) = buildString { + append(" ".repeat(indent)) + append(project).append(" ") + append( + when (configuration) { + "api" -> "-->" + "implementation" -> "-.->" + else -> "-.->|$configuration|" + }, + ) + append(" ").append(dependency) + } + + private fun PluginType.classDef() = "classDef $ref $style;" +} + +@CacheableTask +private abstract class GraphUpdateTask : DefaultTask() { + + @get:Input + abstract val projectPath: Property + + @get:InputFile + @get:PathSensitive(NONE) + abstract val input: RegularFileProperty + + @get:InputFile + @get:PathSensitive(NONE) + abstract val legend: RegularFileProperty + + @get:OutputFile + abstract val output: RegularFileProperty + + override fun getDescription() = "Updates Markdown file with the corresponding dependency graph." + + @TaskAction + operator fun invoke() = with(output.get().asFile) { + if (!exists()) { + createNewFile() + writeText( + """ + # `${projectPath.get()}` + + ## Module dependency graph + + + + """.trimIndent(), + ) + } + val mermaid = input.get().asFile.readText().trimTrailingNewLines() + val legend = legend.get().asFile.readText().trimTrailingNewLines() + val regex = """()(.*?)()""".toRegex(DOT_MATCHES_ALL) + val text = readText().replace(regex) { match -> + val (start, _, end) = match.destructured + """ + |$start + |```mermaid + |$mermaid + |``` + | + |
📋 Graph legend + | + |```mermaid + |$legend + |``` + | + |
+ |$end + """.trimMargin() + } + writeText(text) + } + + private fun String.trimTrailingNewLines() = lines() + .dropLastWhile(String::isBlank) + .joinToString(System.lineSeparator()) +} diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt index ed1ea4254..8d16c428e 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt @@ -17,9 +17,12 @@ package com.google.samples.apps.nowinandroid import com.android.build.api.artifact.ScopedArtifact +import com.android.build.api.dsl.BuildType +import com.android.build.api.dsl.CommonExtension import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.ScopedArtifacts import com.android.build.api.variant.SourceDirectories +import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Project import org.gradle.api.file.Directory import org.gradle.api.file.RegularFile @@ -59,8 +62,15 @@ private fun String.capitalize() = replaceFirstChar { * tests on CI using a different Github Action or an external device farm. */ internal fun Project.configureJacoco( + commonExtension: CommonExtension, androidComponentsExtension: AndroidComponentsExtension<*, *, *>, ) { + // Configure only the debug build, otherwise it will force the debuggable flag on release buildTypes as well + commonExtension.buildTypes.named("debug") { + enableAndroidTestCoverage = true + enableUnitTestCoverage = true + } + configure { toolVersion = libs.findVersion("jacoco").get().toString() } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt index 5d396d2a4..37b1ba99b 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt @@ -32,16 +32,16 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension * Configure base Kotlin with Android options */ internal fun Project.configureKotlinAndroid( - commonExtension: CommonExtension<*, *, *, *, *, *>, + commonExtension: CommonExtension, ) { commonExtension.apply { - compileSdk = 35 + compileSdk = 36 - defaultConfig { - minSdk = 21 + defaultConfig.apply { + minSdk = 23 } - compileOptions { + compileOptions.apply { // Up to Java 11 APIs are available through desugaring // https://developer.android.com/studio/write/java11-minimal-support-table sourceCompatibility = JavaVersion.VERSION_11 diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt index f57e634cc..96ca6073d 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt @@ -4,6 +4,7 @@ import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationProductFlavor import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.ProductFlavor +import org.gradle.kotlin.dsl.invoke @Suppress("EnumEntryName") enum class FlavorDimension { @@ -20,20 +21,20 @@ enum class NiaFlavor(val dimension: FlavorDimension, val applicationIdSuffix: St } fun configureFlavors( - commonExtension: CommonExtension<*, *, *, *, *, *>, + commonExtension: CommonExtension, flavorConfigurationBlock: ProductFlavor.(flavor: NiaFlavor) -> Unit = {}, ) { commonExtension.apply { - FlavorDimension.values().forEach { flavorDimension -> + FlavorDimension.entries.forEach { flavorDimension -> flavorDimensions += flavorDimension.name } productFlavors { - NiaFlavor.values().forEach { niaFlavor -> + NiaFlavor.entries.forEach { niaFlavor -> register(niaFlavor.name) { dimension = niaFlavor.dimension.name flavorConfigurationBlock(this, niaFlavor) - if (this@apply is ApplicationExtension && this is ApplicationProductFlavor) { + if (commonExtension is ApplicationExtension && this is ApplicationProductFlavor) { if (niaFlavor.applicationIdSuffix != null) { applicationIdSuffix = niaFlavor.applicationIdSuffix } diff --git a/build.gradle.kts b/build.gradle.kts index b7989bab4..594f7f92e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,28 +14,6 @@ * limitations under the License. */ -buildscript { - repositories { - google { - content { - includeGroupByRegex("com\\.android.*") - includeGroupByRegex("com\\.google.*") - includeGroupByRegex("androidx.*") - } - } - mavenCentral() - - // This is used only for internal Google builds. - maven { url = uri("../nowinandroid-prebuilts/m2repository") } - } - dependencies { - classpath(libs.google.oss.licenses.plugin) { - exclude(group = "com.google.protobuf") - } - } - -} - /* * By listing all the plugins used throughout all subprojects in the root project build script, it * ensures that the build script classpath remains the same for all projects. This avoids potential @@ -58,7 +36,7 @@ plugins { alias(libs.plugins.hilt) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.roborazzi) apply false - alias(libs.plugins.secrets) apply false + alias(libs.plugins.google.osslicenses) apply false alias(libs.plugins.room) apply false - alias(libs.plugins.module.graph) apply true // Plugin applied to allow module graph generation + alias(libs.plugins.nowinandroid.root) } diff --git a/core/analytics/README.md b/core/analytics/README.md index d2bcd1ea7..f6dab3bcc 100644 --- a/core/analytics/README.md +++ b/core/analytics/README.md @@ -1,3 +1,48 @@ -# :core:analytics module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_analytics.svg) +# `:core:analytics` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + end + +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/core/common/README.md b/core/common/README.md index 96558bcc6..2f3865a8e 100644 --- a/core/common/README.md +++ b/core/common/README.md @@ -1,3 +1,48 @@ -# :core:common module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_common.svg) +# `:core:common` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:common[common]:::jvm-library + end + +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/core/data-test/README.md b/core/data-test/README.md index 977ee10e4..b36f5b66d 100644 --- a/core/data-test/README.md +++ b/core/data-test/README.md @@ -1,3 +1,73 @@ -# :core:data-test module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_data_test.svg) +# `:core:data-test` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:data-test[data-test]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:model[model]:::jvm-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + end + + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:data-test --> :core:data + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :core:model + +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/core/data/README.md b/core/data/README.md index 5d30f1638..0b84940d2 100644 --- a/core/data/README.md +++ b/core/data/README.md @@ -1,3 +1,71 @@ -# :core:data module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_data.svg) +# `:core:data` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:model[model]:::jvm-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + end + + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :core:model + +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/core/data/build.gradle.kts b/core/data/build.gradle.kts index 8c839fa8e..c47d4ee58 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -22,11 +22,7 @@ plugins { android { namespace = "com.google.samples.apps.nowinandroid.core.data" - testOptions { - unitTests { - isIncludeAndroidResources = true - } - } + testOptions.unitTests.isIncludeAndroidResources = true } dependencies { diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt index b2a642cf9..ea9e4de52 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt @@ -23,8 +23,6 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.NetworkRequest.Builder -import android.os.Build.VERSION -import android.os.Build.VERSION_CODES import androidx.core.content.getSystemService import androidx.tracing.trace import com.google.samples.apps.nowinandroid.core.network.Dispatcher @@ -90,13 +88,8 @@ internal class ConnectivityManagerNetworkMonitor @Inject constructor( .flowOn(ioDispatcher) .conflate() - @Suppress("DEPRECATION") - private fun ConnectivityManager.isCurrentlyConnected() = when { - VERSION.SDK_INT >= VERSION_CODES.M -> - activeNetwork - ?.let(::getNetworkCapabilities) - ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - - else -> activeNetworkInfo?.isConnected - } ?: false + private fun ConnectivityManager.isCurrentlyConnected(): Boolean { + val networkCapabilities = getNetworkCapabilities(activeNetwork) ?: return false + return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } } diff --git a/core/database/README.md b/core/database/README.md index 855eab53e..d050c6dbc 100644 --- a/core/database/README.md +++ b/core/database/README.md @@ -1,3 +1,51 @@ -# :core:database module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_database.svg) +# `:core:database` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:database[database]:::android-library + :core:model[model]:::jvm-library + end + + :core:database --> :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/core/datastore-proto/README.md b/core/datastore-proto/README.md index 19ed58239..859e39756 100644 --- a/core/datastore-proto/README.md +++ b/core/datastore-proto/README.md @@ -1,3 +1,48 @@ -# :core:datastore-proto module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_datastore_proto.svg) +# `:core:datastore-proto` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:datastore-proto[datastore-proto]:::android-library + end + +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/core/datastore-test/README.md b/core/datastore-test/README.md index 99cf13f1f..6de8718e8 100644 --- a/core/datastore-test/README.md +++ b/core/datastore-test/README.md @@ -1,3 +1,58 @@ -# :core:datastore-test module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_datastore_test.svg) +# `:core:datastore-test` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:common[common]:::jvm-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:datastore-test[datastore-test]:::android-library + :core:model[model]:::jvm-library + end + + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:datastore-test -.-> :core:common + :core:datastore-test -.-> :core:datastore + +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/core/datastore/README.md b/core/datastore/README.md index 4785c5885..744f60f43 100644 --- a/core/datastore/README.md +++ b/core/datastore/README.md @@ -1,3 +1,55 @@ -# :core:datastore module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_datastore.svg) +# `:core:datastore` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:common[common]:::jvm-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:model[model]:::jvm-library + end + + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :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/core/designsystem/README.md b/core/designsystem/README.md index d1778cb14..3194165fd 100644 --- a/core/designsystem/README.md +++ b/core/designsystem/README.md @@ -1,3 +1,48 @@ -# :core:designsystem module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_designsystem.svg) +# `:core:designsystem` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:designsystem[designsystem]:::android-library + end + +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/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index aac2ddb8f..8a138082b 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -22,6 +22,7 @@ plugins { android { namespace = "com.google.samples.apps.nowinandroid.core.designsystem" + testOptions.unitTests.isIncludeAndroidResources = true } dependencies { diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/theme/Type.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/theme/Type.kt index 82d769863..289f09ce4 100644 --- a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/theme/Type.kt +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/theme/Type.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.LineHeightStyle.Alignment import androidx.compose.ui.text.style.LineHeightStyle.Trim +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.sp /** @@ -33,30 +35,40 @@ internal val NiaTypography = Typography( fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp, + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), displayMedium = TextStyle( fontWeight = FontWeight.Normal, fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp, + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), displaySmall = TextStyle( fontWeight = FontWeight.Normal, fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp, + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), headlineLarge = TextStyle( fontWeight = FontWeight.Normal, fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp, + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), headlineMedium = TextStyle( fontWeight = FontWeight.Normal, fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp, + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), headlineSmall = TextStyle( fontWeight = FontWeight.Normal, @@ -67,6 +79,8 @@ internal val NiaTypography = Typography( alignment = Alignment.Bottom, trim = Trim.None, ), + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), titleLarge = TextStyle( fontWeight = FontWeight.Bold, @@ -77,18 +91,24 @@ internal val NiaTypography = Typography( alignment = Alignment.Bottom, trim = Trim.LastLineBottom, ), + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), titleMedium = TextStyle( fontWeight = FontWeight.Bold, fontSize = 18.sp, lineHeight = 24.sp, letterSpacing = 0.1.sp, + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), titleSmall = TextStyle( fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.1.sp, + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), // Default text style bodyLarge = TextStyle( @@ -100,18 +120,24 @@ internal val NiaTypography = Typography( alignment = Alignment.Center, trim = Trim.None, ), + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), bodyMedium = TextStyle( fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.25.sp, + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), bodySmall = TextStyle( fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.4.sp, + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), // Used for Button labelLarge = TextStyle( @@ -123,6 +149,8 @@ internal val NiaTypography = Typography( alignment = Alignment.Center, trim = Trim.LastLineBottom, ), + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), // Used for Navigation items labelMedium = TextStyle( @@ -134,6 +162,8 @@ internal val NiaTypography = Typography( alignment = Alignment.Center, trim = Trim.LastLineBottom, ), + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), // Used for Tag labelSmall = TextStyle( @@ -145,5 +175,7 @@ internal val NiaTypography = Typography( alignment = Alignment.Center, trim = Trim.LastLineBottom, ), + textDirection = TextDirection.Ltr, + textAlign = TextAlign.Left, ), ) diff --git a/core/designsystem/src/test/resources/robolectric.properties b/core/designsystem/src/test/resources/robolectric.properties new file mode 100644 index 000000000..ca82be153 --- /dev/null +++ b/core/designsystem/src/test/resources/robolectric.properties @@ -0,0 +1,17 @@ +# +# Copyright 2025 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +sdk = 35 \ No newline at end of file diff --git a/core/designsystem/src/test/screenshots/Navigation/Navigation_fontScale2.png b/core/designsystem/src/test/screenshots/Navigation/Navigation_fontScale2.png index b2dff990a..16df296d5 100644 Binary files a/core/designsystem/src/test/screenshots/Navigation/Navigation_fontScale2.png and b/core/designsystem/src/test/screenshots/Navigation/Navigation_fontScale2.png differ diff --git a/core/domain/README.md b/core/domain/README.md index cc6905846..b23877337 100644 --- a/core/domain/README.md +++ b/core/domain/README.md @@ -1,3 +1,74 @@ -# :core:domain module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_domain.svg) +# `:core:domain` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:domain[domain]:::android-library + :core:model[model]:::jvm-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + end + + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:domain --> :core:data + :core:domain --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :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/core/model/README.md b/core/model/README.md index efd0eec76..9cd7659a8 100644 --- a/core/model/README.md +++ b/core/model/README.md @@ -1,3 +1,48 @@ -# :core:model module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_model.svg) +# `:core:model` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:model[model]:::jvm-library + end + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/bookmarks/.gitignore b/core/navigation/.gitignore similarity index 100% rename from feature/bookmarks/.gitignore rename to core/navigation/.gitignore diff --git a/core/navigation/README.md b/core/navigation/README.md new file mode 100644 index 000000000..7cd3b7e9f --- /dev/null +++ b/core/navigation/README.md @@ -0,0 +1,3 @@ +# :core:navigation module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_navigation.svg) diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts new file mode 100644 index 000000000..3e7831dca --- /dev/null +++ b/core/navigation/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.library) + alias(libs.plugins.nowinandroid.hilt) + alias(libs.plugins.hilt) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose) +} + +android { + namespace = "com.google.samples.apps.nowinandroid.core.navigation" +} + +dependencies { + api(libs.androidx.navigation3.runtime) + implementation(libs.androidx.savedstate.compose) + implementation(libs.androidx.lifecycle.viewModel.navigation3) + + testImplementation(libs.truth) + + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.test.ext) + androidTestImplementation(libs.androidx.compose.ui.testManifest) + androidTestImplementation(libs.androidx.lifecycle.viewModel.testing) + androidTestImplementation(libs.truth) +} diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigationState.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigationState.kt new file mode 100644 index 000000000..864fec794 --- /dev/null +++ b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigationState.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.navigation + +import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.rememberDecoratedNavEntries +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator + +/** + * Create a navigation state that persists config changes and process death. + */ +@Composable +fun rememberNavigationState( + startKey: NavKey, + topLevelKeys: Set, +): NavigationState { + val topLevelStack = rememberNavBackStack(startKey) + val subStacks = topLevelKeys.associateWith { key -> rememberNavBackStack(key) } + + return remember(startKey, topLevelKeys) { + NavigationState( + startKey = startKey, + topLevelStack = topLevelStack, + subStacks = subStacks, + ) + } +} + +/** + * State holder for navigation state. + * + * @param startKey - the starting navigation key. The user will exit the app through this key. + * @param topLevelStack - the top level back stack. It holds only top level keys. + * @param subStacks - the back stacks for each top level key + */ +class NavigationState( + val startKey: NavKey, + val topLevelStack: NavBackStack, + val subStacks: Map>, +) { + val currentTopLevelKey: NavKey by derivedStateOf { topLevelStack.last() } + + val topLevelKeys + get() = subStacks.keys + + @get:VisibleForTesting + val currentSubStack: NavBackStack + get() = subStacks[currentTopLevelKey] + ?: error("Sub stack for $currentTopLevelKey does not exist") + + @get:VisibleForTesting + val currentKey: NavKey by derivedStateOf { currentSubStack.last() } +} + +/** + * Convert NavigationState into NavEntries. + */ +@Composable +fun NavigationState.toEntries( + entryProvider: (NavKey) -> NavEntry, +): SnapshotStateList> { + val decoratedEntries = subStacks.mapValues { (_, stack) -> + val decorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ) + rememberDecoratedNavEntries( + backStack = stack, + entryDecorators = decorators, + entryProvider = entryProvider, + ) + } + + return topLevelStack + .flatMap { decoratedEntries[it] ?: emptyList() } + .toMutableStateList() +} diff --git a/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/Navigator.kt b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/Navigator.kt new file mode 100644 index 000000000..c1c456476 --- /dev/null +++ b/core/navigation/src/main/kotlin/com/google/samples/apps/nowinandroid/core/navigation/Navigator.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.navigation + +import androidx.navigation3.runtime.NavKey + +/** + * Handles navigation events (forward and back) by updating the navigation state. + * + * @param state - The navigation state that will be updated in response to navigation events. + */ +class Navigator(val state: NavigationState) { + + /** + * Navigate to a navigation key + * + * @param key - the navigation key to navigate to. + */ + fun navigate(key: NavKey) { + when (key) { + state.currentTopLevelKey -> clearSubStack() + in state.topLevelKeys -> goToTopLevel(key) + else -> goToKey(key) + } + } + + /** + * Go back to the previous navigation key. + */ + fun goBack() { + when (state.currentKey) { + state.startKey -> error("You cannot go back from the start route") + state.currentTopLevelKey -> { + // We're at the base of the current sub stack, go back to the previous top level + // stack. + state.topLevelStack.removeLastOrNull() + } + else -> state.currentSubStack.removeLastOrNull() + } + } + + /** + * Go to a non top level key. + */ + private fun goToKey(key: NavKey) { + state.currentSubStack.apply { + // Remove it if it's already in the stack so it's added at the end. + remove(key) + add(key) + } + } + + /** + * Go to a top level stack. + */ + private fun goToTopLevel(key: NavKey) { + state.topLevelStack.apply { + if (key == state.startKey) { + // This is the start key. Clear the stack so it's added as the only key. + clear() + } else { + // Remove it if it's already in the stack so it's added at the end. + remove(key) + } + add(key) + } + } + + /** + * Clearing all but the root key in the current sub stack. + */ + private fun clearSubStack() { + state.currentSubStack.run { + if (size > 1) subList(1, size).clear() + } + } +} diff --git a/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigatorTest.kt b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigatorTest.kt new file mode 100644 index 000000000..86c4acc25 --- /dev/null +++ b/core/navigation/src/test/kotlin/com/google/samples/apps/nowinandroid/core/navigation/NavigatorTest.kt @@ -0,0 +1,256 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.navigation + +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import kotlin.test.assertFailsWith + +private object TestFirstTopLevelKey : NavKey +private object TestSecondTopLevelKey : NavKey +private object TestThirdTopLevelKey : NavKey +private object TestKeyFirst : NavKey +private object TestKeySecond : NavKey + +class NavigatorTest { + + private lateinit var navigationState: NavigationState + private lateinit var navigator: Navigator + + @Before + fun setup() { + val startKey = TestFirstTopLevelKey + val topLevelStack = NavBackStack(startKey) + val topLevelKeys = listOf( + startKey, + TestSecondTopLevelKey, + TestThirdTopLevelKey, + ) + val subStacks = topLevelKeys.associateWith { key -> NavBackStack(key) } + + navigationState = NavigationState( + startKey = startKey, + topLevelStack = topLevelStack, + subStacks = subStacks, + ) + navigator = Navigator(navigationState) + } + + @Test + fun testStartKey() { + assertThat(navigationState.startKey).isEqualTo(TestFirstTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun testNavigate() { + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + assertThat(navigationState.subStacks[TestFirstTopLevelKey]?.last()).isEqualTo(TestKeyFirst) + } + + @Test + fun testNavigateTopLevel() { + navigator.navigate(TestSecondTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey) + } + + @Test + fun testNavigateSingleTop() { + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + ).inOrder() + + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + ).inOrder() + } + + @Test + fun testNavigateTopLevelSingleTop() { + navigator.navigate(TestSecondTopLevelKey) + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentSubStack).containsExactly( + TestSecondTopLevelKey, + TestKeyFirst, + ).inOrder() + + navigator.navigate(TestSecondTopLevelKey) + + assertThat(navigationState.currentSubStack).containsExactly( + TestSecondTopLevelKey, + ).inOrder() + } + + @Test + fun testSubStack() { + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + + navigator.navigate(TestKeySecond) + + assertThat(navigationState.currentKey).isEqualTo(TestKeySecond) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun testMultiStack() { + // add to start stack + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + + // navigate to new top level + navigator.navigate(TestSecondTopLevelKey) + + assertThat(navigationState.currentKey).isEqualTo(TestSecondTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey) + + // add to new stack + navigator.navigate(TestKeySecond) + + assertThat(navigationState.currentKey).isEqualTo(TestKeySecond) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey) + + // go back to start stack + navigator.navigate(TestFirstTopLevelKey) + + assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun testPopOneNonTopLevel() { + navigator.navigate(TestKeyFirst) + navigator.navigate(TestKeySecond) + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + TestKeySecond, + ).inOrder() + + navigator.goBack() + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + ).inOrder() + + assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun testPopOneTopLevel() { + navigator.navigate(TestKeyFirst) + navigator.navigate(TestSecondTopLevelKey) + + assertThat(navigationState.currentSubStack).containsExactly( + TestSecondTopLevelKey, + ).inOrder() + + assertThat(navigationState.currentKey).isEqualTo(TestSecondTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestSecondTopLevelKey) + + // remove TopLevel + navigator.goBack() + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + ).inOrder() + + assertThat(navigationState.currentKey).isEqualTo(TestKeyFirst) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun popMultipleNonTopLevel() { + navigator.navigate(TestKeyFirst) + navigator.navigate(TestKeySecond) + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + TestKeyFirst, + TestKeySecond, + ).inOrder() + + navigator.goBack() + navigator.goBack() + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + ).inOrder() + + assertThat(navigationState.currentKey).isEqualTo(TestFirstTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun popMultipleTopLevel() { + // second sub-stack + navigator.navigate(TestSecondTopLevelKey) + navigator.navigate(TestKeyFirst) + + assertThat(navigationState.currentSubStack).containsExactly( + TestSecondTopLevelKey, + TestKeyFirst, + ).inOrder() + + // third sub-stack + navigator.navigate(TestThirdTopLevelKey) + navigator.navigate(TestKeySecond) + + assertThat(navigationState.currentSubStack).containsExactly( + TestThirdTopLevelKey, + TestKeySecond, + ).inOrder() + + repeat(4) { + navigator.goBack() + } + + assertThat(navigationState.currentSubStack).containsExactly( + TestFirstTopLevelKey, + ).inOrder() + + assertThat(navigationState.currentKey).isEqualTo(TestFirstTopLevelKey) + assertThat(navigationState.currentTopLevelKey).isEqualTo(TestFirstTopLevelKey) + } + + @Test + fun throwOnEmptyBackStack() { + assertFailsWith { + navigator.goBack() + } + } +} diff --git a/core/network/README.md b/core/network/README.md index 516aa2d38..d52be6270 100644 --- a/core/network/README.md +++ b/core/network/README.md @@ -1,3 +1,53 @@ -# :core:network module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_network.svg) +# `:core:network` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:common[common]:::jvm-library + :core:model[model]:::jvm-library + :core:network[network]:::android-library + end + + :core:network --> :core:common + :core:network --> :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/core/network/build.gradle.kts b/core/network/build.gradle.kts index bf4dd9153..edc28ea23 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -30,11 +30,7 @@ android { buildConfig = true } namespace = "com.google.samples.apps.nowinandroid.core.network" - testOptions { - unitTests { - isIncludeAndroidResources = true - } - } + testOptions.unitTests.isIncludeAndroidResources = true } dependencies { @@ -57,15 +53,12 @@ val backendUrl = providers.fileContents( ).asText.map { text -> val properties = Properties() properties.load(StringReader(text)) - if (properties.containsKey("BACKEND_URL")) - (properties["BACKEND_URL"] as String) - else "http://example.com" - // Move to returning `properties["BACKEND_URL"] as String?` after upgrading to Gradle 9.0.0 + properties["BACKEND_URL"] }.orElse("http://example.com") androidComponents { onVariants { - it.buildConfigFields.put("BACKEND_URL", backendUrl.map { value -> + it.buildConfigFields!!.put("BACKEND_URL", backendUrl.map { value -> BuildConfigField(type = "String", value = """"$value"""", comment = null) }) } diff --git a/core/notifications/README.md b/core/notifications/README.md index 8f5607bdf..d184a6672 100644 --- a/core/notifications/README.md +++ b/core/notifications/README.md @@ -1,3 +1,53 @@ -# :core:notifications module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_notifications.svg) +# `:core:notifications` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:common[common]:::jvm-library + :core:model[model]:::jvm-library + :core:notifications[notifications]:::android-library + end + + :core:notifications -.-> :core:common + :core:notifications --> :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/core/screenshot-testing/README.md b/core/screenshot-testing/README.md index 9bd4f1f9c..793651023 100644 --- a/core/screenshot-testing/README.md +++ b/core/screenshot-testing/README.md @@ -1,3 +1,51 @@ -# :core:screenshot-testing module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_screenshot_testing.svg) +# `:core:screenshot-testing` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:designsystem[designsystem]:::android-library + :core:screenshot-testing[screenshot-testing]:::android-library + end + + :core:screenshot-testing -.-> :core:designsystem + +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/core/testing/README.md b/core/testing/README.md index 5a35d379b..d5925b1a5 100644 --- a/core/testing/README.md +++ b/core/testing/README.md @@ -1,3 +1,77 @@ -# :core:testing module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_testing.svg) +# `:core:testing` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:model[model]:::jvm-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + :core:testing[testing]:::android-library + end + + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :core:model + :core:testing --> :core:analytics + :core:testing --> :core:common + :core:testing --> :core:data + :core:testing --> :core:model + :core:testing --> :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/core/ui/README.md b/core/ui/README.md index 38e514d01..c6b365553 100644 --- a/core/ui/README.md +++ b/core/ui/README.md @@ -1,3 +1,55 @@ -# :core:ui module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_core_ui.svg) +# `:core:ui` + +## 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 + + :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/docs/ModularizationLearningJourney.md b/docs/ModularizationLearningJourney.md index a9766c68d..b50242776 100644 --- a/docs/ModularizationLearningJourney.md +++ b/docs/ModularizationLearningJourney.md @@ -1,121 +1,93 @@ # Modularization learning journey -In this learning journey you will learn about modularization, and the modularization strategy used -to create the modules in the Now in Android app. - - -## Overview - -Modularization is the practice of breaking the concept of a monolithic, one-module codebase into -loosely coupled, self contained modules. - - -### Benefits of modularization - -This offers many benefits, including: - -**Scalability** - In a tightly coupled codebase, a single change can trigger a cascade of -alterations. A properly modularized project will embrace -the [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) principle. This -in turn empowers the contributors with more autonomy while also enforcing architectural patterns. - -**Enabling work in parallel** - Modularization helps decrease version control conflicts and enables -more efficient work in parallel for developers in larger teams. - -**Ownership** - A module can have a dedicated owner who is responsible for maintaining the code and -tests, fixing bugs, and reviewing changes. - -**Encapsulation** - Isolated code is easier to read, understand, test and maintain. - -**Reduced build time** - Leveraging Gradle’s parallel and incremental build can reduce build times. - -**Dynamic delivery** - Modularization is a requirement -for [Play Feature Delivery](https://developer.android.com/guide/playcore/feature-delivery) which -allows certain features of your app to be delivered conditionally or downloaded on demand. - -**Reusability** - Proper modularization enables opportunities for code sharing and building multiple -apps, across different platforms, from the same foundation. - - -### Modularization pitfalls - -However, modularization is a pattern that can be misused, and there are some gotchas to be aware of -when modularizing an app: - -**Too many modules** - each module has an overhead that comes in the form of increased complexity of -the build configuration. This can cause Gradle sync times to increase, and incurs an ongoing -maintenance cost. In addition, adding more modules increases the complexity of the project’s Gradle -setup, when compared to a single monolithic module. This can be mitigated by making use of -convention plugins, to extract reusable and composable build configuration into type-safe Kotlin -code. In the Now in Android app, these convention plugins can be found in -the [`build-logic` folder](https://github.com/android/nowinandroid/tree/main/build-logic). - -**Not enough modules** - conversely if your modules are few, large and tightly coupled, you end up -with yet another monolith. This means you lose some benefits of modularization. If your module is -bloated and has no single, well defined purpose, you should consider splitting it. - -**Too complex** - there is no silver bullet here. In fact it doesn’t always make sense to modularize -your project. A dominating factor is the size and relative complexity of the codebase. If your -project is not expected to grow beyond a certain threshold, the scalability and build time gains -won’t apply. - - -## Modularization strategy - -It’s important to note that there is no single modularization strategy that fits all projects. -However, there are general guidelines that can be followed to ensure you maximize its benefits and -minimize its downsides. - -A barebone module is simply a directory with a Gradle build script inside. Usually though, a module -will consist of one or more source sets and possibly a collection of resources or assets. Modules -can be built and tested independently. Due to Gradle's flexibility there are few constraints as to -how you can organize your project. In general, you should strive for low coupling and high cohesion. - -* **Low coupling** - Modules should be as independent as possible from one another, so that changes - to one module have zero or minimal impact on other modules. They should not possess knowledge of - the inner workings of other modules. - -* **High cohesion** - A module should comprise a collection of code that acts as a system. It should - have clearly defined responsibilities and stay within boundaries of certain domain knowledge. For - example, - the [`core:network` module](https://github.com/android/nowinandroid/tree/main/core/network) in Now - in Android is responsible for making network requests, handling responses from a remote data - source, and supplying data to other modules. - - -## Types of modules in Now in Android - -![Diagram showing types of modules and their dependencies in Now in Android](images/modularization-graph.drawio.png "Diagram showing types of modules and their dependencies in Now in Android") +In this learning journey you will learn about the modularization strategy used +to create modules in the Now in Android app. For the theory behind modularization, check out +[the official guidance](https://developer.android.com/topic/modularization). + +**IMPORTANT:** Every module has a dependency graph in its README ([example for the app module](https://github.com/android/nowinandroid/tree/main/app)) which can be useful for understanding the overall structure of the project. + +## Module types + +```mermaid +graph TB + subgraph :core + direction TB + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:model[model]:::jvm-library + :core:network[network]:::android-library + :core:ui[ui]:::android-library + end + subgraph :feature + direction TB + :feature:topic[topic]:::android-feature + :feature:foryou[foryou]:::android-feature + :feature:interests[interests]:::android-feature + :feature:foo[...]:::android-feature + + end + :app[app]:::android-application + + :app -.-> :feature:foryou + :app -.-> :feature:interests + :app -.-> :feature:topic + :core:data ---> :core:database + :core:data ---> :core:network + :core:database ---> :core:model + :core:network ---> :core:model + :core:ui ---> :core:model + :feature:topic -.-> :core:data + :feature:topic -.-> :core:ui + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application:::android-application -. implementation .-> feature:::android-feature + library:::android-library -- api --> jvm:::jvm-library + +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 jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +``` + +
**Top tip**: A module graph (shown above) can be useful during modularization planning for visualizing dependencies between modules. The Now in Android app contains the following types of modules: -* The `app` module - contains app level and scaffolding classes that bind the rest of the codebase, - such as `MainActivity`, `NiaApp` and app-level controlled navigation. A good example of this is - the navigation setup through `NiaNavHost` and the bottom navigation bar setup - through `TopLevelDestination`. The `app` module depends on all `feature` modules and - required `core` modules. +### The `app` module +This contains app level and scaffolding classes that bind the rest of the codebase, such as +`MainActivity`, `NiaApp` and app-level controlled navigation. A good example of this is the navigation setup through `NiaNavHost` and the bottom navigation bar setup through `TopLevelDestination`. The `app` module depends on all `feature` modules and required `core` modules. -* `feature:` modules - feature specific modules which are scoped to handle a single responsibility - in the app. These modules can be reused by any app, including test or other flavoured apps, when - needed, while still keeping it separated and isolated. If a class is needed only by one `feature` - module, it should remain within that module. If not, it should be extracted into an - appropriate `core` module. A `feature` module should have no dependencies on other feature - modules. They only depend on the `core` modules that they require. +### Feature modules +These are feature-specific modules that handle a single responsibility in the app. For example, the `ForYou` feature handles all content and UI state for the "ForYou" screen. Feature modules aren't Gradle modules themselves, they are split into two submodules: -* `core:` modules - common library modules containing auxiliary code and specific dependencies that - need to be shared between other modules in the app. These modules can depend on other core - modules, but they shouldn’t depend on feature nor app modules. +* `api` - contains navigation keys +* `impl` - contains everything else -* Miscellaneous modules - such as `sync`, `benchmark` and `test` modules, as well - as `app-nia-catalog` - a catalog app for displaying our design system quickly. +This approach allows features to navigate to other features by using the target feature's navigation keys. A feature's `api` and `impl` modules can be used by any app, including test or other flavoured apps. If a class is needed only by one feature module, it should remain within that module. If not, it should be placed into an appropriate `core` module. +A feature's `api` module should not depend on another feature's `api` or `impl` module. A feature's `impl` should only depend on another feature's `api` module. Both submodules should only depend on the `core` modules that they require. -## Modules +### Core modules +These are common library modules containing auxiliary code and specific dependencies that + need to be shared between other modules in the app. These modules can depend on other core + modules, but they shouldn’t depend on feature nor app modules. + +### Miscellaneous modules +For example, `sync`, `benchmark` and `test` modules, as well as `app-nia-catalog` - a catalog app for displaying our design system quickly. -Using the above modularization strategy, the Now in Android app has the following modules: +## Examples @@ -136,15 +108,28 @@ Using the above modularization strategy, the Now in Android app has the followin - + + + + +
feature:1,
- feature:2
+
feature:1:api,
+ feature:2:api
+ ... +
Navigation keys and functions that other features can use to navigate to this feature.

+ For example: The :topic:api module exposes a Navigator.navigateToTopic function that the + :interests:impl module uses to navigate from the InterestsScreen to the TopicScreen when + a topic is clicked. +
TopicNavKey +
feature:1:impl,
+ feature:2:impl
...
Functionality associated with a specific feature or user journey. Typically contains UI components and ViewModels which read data from other modules.
Examples include:
    -
  • feature:topic displays information about a topic on the TopicScreen.
  • -
  • feature:foryou which displays the user's news feed, and onboarding during first run, on the For You screen.
  • +
  • feature:topic:impl displays information about a topic on the TopicScreen.
  • +
  • feature:foryou:impl which displays the user's news feed, and onboarding during first run, on the For You screen.
TopicScreen
@@ -233,8 +218,12 @@ Using the above modularization strategy, the Now in Android app has the followin
+## Dependency graphs +Each module has its own `README.md` file containing a module graph (e.g. [`:app` module graph](../app/README.md#module-dependency-graph)). +When modules dependencies change, module graphs are automatically updated by the [Build.yaml](../.github/workflows/Build.yaml) workflow. +You can also manually update the graphs by running the `graphUpdate` task. -## Modularization in Now in Android +## Further considerations Our modularization approach was defined taking into account the “Now in Android” project roadmap, upcoming work and new features. Additionally, our aim this time around was to find the right balance between overmodularizing a relatively small app and using this opportunity to showcase a modularization pattern fit for a much larger codebase, closer to real world apps in production environments. diff --git a/docs/images/graphs/dep_graph_app.svg b/docs/images/graphs/dep_graph_app.svg deleted file mode 100644 index 8e5d9d429..000000000 --- a/docs/images/graphs/dep_graph_app.svg +++ /dev/null @@ -1,305 +0,0 @@ - - - - - - :app - - - - :feature:interests - - - - - - - - :feature:foryou - - - - - - - - :feature:bookmarks - - - - - - - - :feature:topic - - - - - - - - :feature:search - - - - - - - - :feature:settings - - - - - - - - :core:common - - - - - - - - :core:ui - - - - - - - - :core:designsystem - - - - - - - - :core:data - - - - - - - - :core:model - - - - - - - - :core:analytics - - - - - - - - :sync:work - - - - - - - - - - - - - - - - - - - - :core:domain - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:notifications - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_app_nia_catalog.svg b/docs/images/graphs/dep_graph_app_nia_catalog.svg deleted file mode 100644 index 151ee63ad..000000000 --- a/docs/images/graphs/dep_graph_app_nia_catalog.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - :app-nia-catalog - - - - :core:designsystem - - - - - - - - :core:ui - - - - - - - - - - - - :core:analytics - - - - - - - - :core:model - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_analytics.svg b/docs/images/graphs/dep_graph_core_analytics.svg deleted file mode 100644 index 45f1c1eb0..000000000 --- a/docs/images/graphs/dep_graph_core_analytics.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - :core:analytics - - - diff --git a/docs/images/graphs/dep_graph_core_common.svg b/docs/images/graphs/dep_graph_core_common.svg deleted file mode 100644 index 91033eaa0..000000000 --- a/docs/images/graphs/dep_graph_core_common.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - :core:common - - - diff --git a/docs/images/graphs/dep_graph_core_data.svg b/docs/images/graphs/dep_graph_core_data.svg deleted file mode 100644 index ab91bafb2..000000000 --- a/docs/images/graphs/dep_graph_core_data.svg +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - :core:data - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:analytics - - - - - - - - :core:notifications - - - - - - - - :core:model - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_data_test.svg b/docs/images/graphs/dep_graph_core_data_test.svg deleted file mode 100644 index b9736c859..000000000 --- a/docs/images/graphs/dep_graph_core_data_test.svg +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - :core:data-test - - - - :core:data - - - - - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:analytics - - - - - - - - :core:notifications - - - - - - - - :core:model - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_database.svg b/docs/images/graphs/dep_graph_core_database.svg deleted file mode 100644 index e82d46436..000000000 --- a/docs/images/graphs/dep_graph_core_database.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - :core:database - - - - :core:model - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_datastore.svg b/docs/images/graphs/dep_graph_core_datastore.svg deleted file mode 100644 index f7502e55b..000000000 --- a/docs/images/graphs/dep_graph_core_datastore.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - :core:datastore - - - - :core:datastore-proto - - - - - - - - :core:model - - - - - - - - :core:common - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_datastore_proto.svg b/docs/images/graphs/dep_graph_core_datastore_proto.svg deleted file mode 100644 index 7fcfb8358..000000000 --- a/docs/images/graphs/dep_graph_core_datastore_proto.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - :core:datastore-proto - - - diff --git a/docs/images/graphs/dep_graph_core_datastore_test.svg b/docs/images/graphs/dep_graph_core_datastore_test.svg deleted file mode 100644 index 37521a05f..000000000 --- a/docs/images/graphs/dep_graph_core_datastore_test.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - :core:datastore-test - - - - :core:common - - - - - - - - :core:datastore - - - - - - - - - - - - :core:datastore-proto - - - - - - - - :core:model - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_designsystem.svg b/docs/images/graphs/dep_graph_core_designsystem.svg deleted file mode 100644 index 737140876..000000000 --- a/docs/images/graphs/dep_graph_core_designsystem.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - :core:designsystem - - - diff --git a/docs/images/graphs/dep_graph_core_domain.svg b/docs/images/graphs/dep_graph_core_domain.svg deleted file mode 100644 index fe3740d2f..000000000 --- a/docs/images/graphs/dep_graph_core_domain.svg +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - :core:domain - - - - :core:data - - - - - - - - :core:model - - - - - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:analytics - - - - - - - - :core:notifications - - - - - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_model.svg b/docs/images/graphs/dep_graph_core_model.svg deleted file mode 100644 index 125684a08..000000000 --- a/docs/images/graphs/dep_graph_core_model.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - :core:model - - - diff --git a/docs/images/graphs/dep_graph_core_network.svg b/docs/images/graphs/dep_graph_core_network.svg deleted file mode 100644 index 3022a86ee..000000000 --- a/docs/images/graphs/dep_graph_core_network.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - :core:network - - - - :core:common - - - - - - - - :core:model - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_notifications.svg b/docs/images/graphs/dep_graph_core_notifications.svg deleted file mode 100644 index d96d28769..000000000 --- a/docs/images/graphs/dep_graph_core_notifications.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - :core:notifications - - - - :core:model - - - - - - - - :core:common - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_screenshot_testing.svg b/docs/images/graphs/dep_graph_core_screenshot_testing.svg deleted file mode 100644 index a7d58b0ea..000000000 --- a/docs/images/graphs/dep_graph_core_screenshot_testing.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - :core:screenshot-testing - - - - :core:designsystem - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_testing.svg b/docs/images/graphs/dep_graph_core_testing.svg deleted file mode 100644 index d441858e5..000000000 --- a/docs/images/graphs/dep_graph_core_testing.svg +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - :core:testing - - - - :core:analytics - - - - - - - - :core:common - - - - - - - - :core:data - - - - - - - - :core:model - - - - - - - - :core:notifications - - - - - - - - - - - - - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - - - - - - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_core_ui.svg b/docs/images/graphs/dep_graph_core_ui.svg deleted file mode 100644 index 2cd972357..000000000 --- a/docs/images/graphs/dep_graph_core_ui.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - :core:ui - - - - :core:analytics - - - - - - - - :core:designsystem - - - - - - - - :core:model - - - - - - - diff --git a/docs/images/graphs/dep_graph_feature_bookmarks.svg b/docs/images/graphs/dep_graph_feature_bookmarks.svg deleted file mode 100644 index 0391eb39c..000000000 --- a/docs/images/graphs/dep_graph_feature_bookmarks.svg +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - :feature:bookmarks - - - - :core:ui - - - - - - - - :core:designsystem - - - - - - - - :core:data - - - - - - - - - - - - :core:analytics - - - - - - - - :core:model - - - - - - - - - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:notifications - - - - - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_feature_foryou.svg b/docs/images/graphs/dep_graph_feature_foryou.svg deleted file mode 100644 index 63a154a87..000000000 --- a/docs/images/graphs/dep_graph_feature_foryou.svg +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - :feature:foryou - - - - :core:ui - - - - - - - - :core:designsystem - - - - - - - - :core:data - - - - - - - - :core:domain - - - - - - - - - - - - :core:analytics - - - - - - - - :core:model - - - - - - - - - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:notifications - - - - - - - - - - - - - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_feature_interests.svg b/docs/images/graphs/dep_graph_feature_interests.svg deleted file mode 100644 index 2de1fc61f..000000000 --- a/docs/images/graphs/dep_graph_feature_interests.svg +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - :feature:interests - - - - :core:ui - - - - - - - - :core:designsystem - - - - - - - - :core:data - - - - - - - - :core:domain - - - - - - - - - - - - :core:analytics - - - - - - - - :core:model - - - - - - - - - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:notifications - - - - - - - - - - - - - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_feature_search.svg b/docs/images/graphs/dep_graph_feature_search.svg deleted file mode 100644 index 7f8f29777..000000000 --- a/docs/images/graphs/dep_graph_feature_search.svg +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - :feature:search - - - - :core:ui - - - - - - - - :core:designsystem - - - - - - - - :core:data - - - - - - - - :core:domain - - - - - - - - - - - - :core:analytics - - - - - - - - :core:model - - - - - - - - - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:notifications - - - - - - - - - - - - - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_feature_settings.svg b/docs/images/graphs/dep_graph_feature_settings.svg deleted file mode 100644 index 3f0d35df2..000000000 --- a/docs/images/graphs/dep_graph_feature_settings.svg +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - :feature:settings - - - - :core:ui - - - - - - - - :core:designsystem - - - - - - - - :core:data - - - - - - - - - - - - :core:analytics - - - - - - - - :core:model - - - - - - - - - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:notifications - - - - - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_feature_topic.svg b/docs/images/graphs/dep_graph_feature_topic.svg deleted file mode 100644 index b7c7dd26c..000000000 --- a/docs/images/graphs/dep_graph_feature_topic.svg +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - :feature:topic - - - - :core:ui - - - - - - - - :core:designsystem - - - - - - - - :core:data - - - - - - - - - - - - :core:analytics - - - - - - - - :core:model - - - - - - - - - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:notifications - - - - - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_sync_sync_test.svg b/docs/images/graphs/dep_graph_sync_sync_test.svg deleted file mode 100644 index 7a083ba54..000000000 --- a/docs/images/graphs/dep_graph_sync_sync_test.svg +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - :sync:sync-test - - - - :core:data - - - - - - - - :sync:work - - - - - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:analytics - - - - - - - - :core:notifications - - - - - - - - - - - - - - - - :core:model - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/graphs/dep_graph_sync_work.svg b/docs/images/graphs/dep_graph_sync_work.svg deleted file mode 100644 index c649f2397..000000000 --- a/docs/images/graphs/dep_graph_sync_work.svg +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - :sync:work - - - - :core:analytics - - - - - - - - :core:data - - - - - - - - - - - - :core:common - - - - - - - - :core:database - - - - - - - - :core:datastore - - - - - - - - :core:network - - - - - - - - :core:notifications - - - - - - - - :core:model - - - - - - - - - - - - - - - - :core:datastore-proto - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/modularization-graph.drawio.png b/docs/images/modularization-graph.drawio.png deleted file mode 100644 index fb5bc375b..000000000 Binary files a/docs/images/modularization-graph.drawio.png and /dev/null differ diff --git a/feature/bookmarks/README.md b/feature/bookmarks/README.md deleted file mode 100644 index 54cbf91d0..000000000 --- a/feature/bookmarks/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# :feature:bookmarks module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_feature_bookmarks.svg) diff --git a/feature/foryou/.gitignore b/feature/bookmarks/api/.gitignore similarity index 100% rename from feature/foryou/.gitignore rename to feature/bookmarks/api/.gitignore diff --git a/feature/bookmarks/api/README.md b/feature/bookmarks/api/README.md new file mode 100644 index 000000000..7a97f09be --- /dev/null +++ b/feature/bookmarks/api/README.md @@ -0,0 +1,57 @@ +# `:feature:bookmarks:api` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:bookmarks + direction TB + :feature:bookmarks:api[api]:::android-library + end + end + subgraph :core + direction TB + :core:navigation[navigation]:::android-library + end + + :feature:bookmarks:api --> :core:navigation + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/bookmarks/api/build.gradle.kts b/feature/bookmarks/api/build.gradle.kts new file mode 100644 index 000000000..a51468615 --- /dev/null +++ b/feature/bookmarks/api/build.gradle.kts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.feature.api) +} + +android { + namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks.api" +} \ No newline at end of file diff --git a/feature/bookmarks/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/api/navigation/BookmarksNavKey.kt b/feature/bookmarks/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/api/navigation/BookmarksNavKey.kt new file mode 100644 index 000000000..988266ffc --- /dev/null +++ b/feature/bookmarks/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/api/navigation/BookmarksNavKey.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +object BookmarksNavKey : NavKey diff --git a/feature/bookmarks/api/src/main/res/drawable/feature_bookmarks_api_mg_empty_bookmarks.xml b/feature/bookmarks/api/src/main/res/drawable/feature_bookmarks_api_mg_empty_bookmarks.xml new file mode 100644 index 000000000..bc12d4325 --- /dev/null +++ b/feature/bookmarks/api/src/main/res/drawable/feature_bookmarks_api_mg_empty_bookmarks.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/feature/bookmarks/src/main/res/values/strings.xml b/feature/bookmarks/api/src/main/res/values/strings.xml similarity index 59% rename from feature/bookmarks/src/main/res/values/strings.xml rename to feature/bookmarks/api/src/main/res/values/strings.xml index 6e2b23043..98f4b4a8d 100644 --- a/feature/bookmarks/src/main/res/values/strings.xml +++ b/feature/bookmarks/api/src/main/res/values/strings.xml @@ -15,10 +15,10 @@ limitations under the License. --> - Saved - Loading saved… - No saved updates - Updates you save will be stored here\nto read later - Bookmark removed - UNDO + Saved + Loading saved… + No saved updates + Updates you save will be stored here\nto read later + Bookmark removed + UNDO diff --git a/feature/interests/.gitignore b/feature/bookmarks/impl/.gitignore similarity index 100% rename from feature/interests/.gitignore rename to feature/bookmarks/impl/.gitignore diff --git a/feature/bookmarks/impl/README.md b/feature/bookmarks/impl/README.md new file mode 100644 index 000000000..123a842f6 --- /dev/null +++ b/feature/bookmarks/impl/README.md @@ -0,0 +1,98 @@ +# `:feature:bookmarks:impl` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:bookmarks + direction TB + :feature:bookmarks:api[api]:::android-library + :feature:bookmarks:impl[impl]:::android-library + end + subgraph :feature:topic + direction TB + :feature:topic:api[api]:::android-library + end + end + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:designsystem[designsystem]:::android-library + :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + :core:ui[ui]:::android-library + end + + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :core:model + :core:ui --> :core:analytics + :core:ui --> :core:designsystem + :core:ui --> :core:model + :feature:bookmarks:api --> :core:navigation + :feature:bookmarks:impl -.-> :core:data + :feature:bookmarks:impl -.-> :core:designsystem + :feature:bookmarks:impl -.-> :core:ui + :feature:bookmarks:impl -.-> :feature:bookmarks:api + :feature:bookmarks:impl -.-> :feature:topic:api + :feature:topic:api -.-> :core:designsystem + :feature:topic:api --> :core:navigation + :feature:topic:api -.-> :core:ui + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/bookmarks/build.gradle.kts b/feature/bookmarks/impl/build.gradle.kts similarity index 85% rename from feature/bookmarks/build.gradle.kts rename to feature/bookmarks/impl/build.gradle.kts index 51a15ce7a..e8162afff 100644 --- a/feature/bookmarks/build.gradle.kts +++ b/feature/bookmarks/impl/build.gradle.kts @@ -15,17 +15,18 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) + alias(libs.plugins.nowinandroid.android.feature.impl) alias(libs.plugins.nowinandroid.android.library.compose) - alias(libs.plugins.nowinandroid.android.library.jacoco) } android { - namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks" + namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks.impl" } dependencies { implementation(projects.core.data) + implementation(projects.feature.bookmarks.api) + implementation(projects.feature.topic.api) testImplementation(projects.core.testing) diff --git a/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreenTest.kt similarity index 96% rename from feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt rename to feature/bookmarks/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreenTest.kt index dd01b84c9..0b73a7918 100644 --- a/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt +++ b/feature/bookmarks/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreenTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.bookmarks +package com.google.samples.apps.nowinandroid.feature.bookmarks.impl import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider @@ -36,6 +36,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.testing.TestLifecycleOwner import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -64,7 +65,7 @@ class BookmarksScreenTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(R.string.feature_bookmarks_loading), + composeTestRule.activity.resources.getString(R.string.feature_bookmarks_api_loading), ) .assertExists() } @@ -161,13 +162,13 @@ class BookmarksScreenTest { composeTestRule .onNodeWithText( - composeTestRule.activity.getString(R.string.feature_bookmarks_empty_error), + composeTestRule.activity.getString(R.string.feature_bookmarks_api_empty_error), ) .assertExists() composeTestRule .onNodeWithText( - composeTestRule.activity.getString(R.string.feature_bookmarks_empty_description), + composeTestRule.activity.getString(R.string.feature_bookmarks_api_empty_description), ) .assertExists() } diff --git a/feature/bookmarks/src/main/AndroidManifest.xml b/feature/bookmarks/impl/src/main/AndroidManifest.xml similarity index 100% rename from feature/bookmarks/src/main/AndroidManifest.xml rename to feature/bookmarks/impl/src/main/AndroidManifest.xml diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreen.kt similarity index 96% rename from feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt rename to feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreen.kt index 7c229c5ea..65bc4acf2 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.bookmarks +package com.google.samples.apps.nowinandroid.feature.bookmarks.impl import androidx.annotation.VisibleForTesting import androidx.compose.foundation.Image @@ -56,7 +56,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -74,9 +74,10 @@ import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider import com.google.samples.apps.nowinandroid.core.ui.newsFeed +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R @Composable -internal fun BookmarksRoute( +internal fun BookmarksScreen( onTopicClick: (String) -> Unit, onShowSnackbar: suspend (String, String?) -> Boolean, modifier: Modifier = Modifier, @@ -112,8 +113,8 @@ internal fun BookmarksScreen( undoBookmarkRemoval: () -> Unit = {}, clearUndoState: () -> Unit = {}, ) { - val bookmarkRemovedMessage = stringResource(id = R.string.feature_bookmarks_removed) - val undoText = stringResource(id = R.string.feature_bookmarks_undo) + val bookmarkRemovedMessage = stringResource(id = R.string.feature_bookmarks_api_removed) + val undoText = stringResource(id = R.string.feature_bookmarks_api_undo) LaunchedEffect(shouldDisplayUndoBookmark) { if (shouldDisplayUndoBookmark) { @@ -155,7 +156,7 @@ private fun LoadingState(modifier: Modifier = Modifier) { .fillMaxWidth() .wrapContentSize() .testTag("forYou:loading"), - contentDesc = stringResource(id = R.string.feature_bookmarks_loading), + contentDesc = stringResource(id = R.string.feature_bookmarks_api_loading), ) } @@ -228,7 +229,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { val iconTint = LocalTintTheme.current.iconTint Image( modifier = Modifier.fillMaxWidth(), - painter = painterResource(id = R.drawable.feature_bookmarks_img_empty_bookmarks), + painter = painterResource(id = R.drawable.feature_bookmarks_api_mg_empty_bookmarks), colorFilter = if (iconTint != Color.Unspecified) ColorFilter.tint(iconTint) else null, contentDescription = null, ) @@ -236,7 +237,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.height(48.dp)) Text( - text = stringResource(id = R.string.feature_bookmarks_empty_error), + text = stringResource(id = R.string.feature_bookmarks_api_empty_error), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleMedium, @@ -246,7 +247,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(id = R.string.feature_bookmarks_empty_description), + text = stringResource(id = R.string.feature_bookmarks_api_empty_description), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium, diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModel.kt similarity index 97% rename from feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt rename to feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModel.kt index f93602485..f36c9d31f 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.bookmarks +package com.google.samples.apps.nowinandroid.feature.bookmarks.impl import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt new file mode 100644 index 000000000..467965651 --- /dev/null +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/navigation/BookmarksEntryProvider.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation + +import androidx.compose.material3.SnackbarDuration.Short +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult.ActionPerformed +import androidx.compose.runtime.compositionLocalOf +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.BookmarksScreen +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic + +fun EntryProviderScope.bookmarksEntry(navigator: Navigator) { + entry { + val snackbarHostState = LocalSnackbarHostState.current + BookmarksScreen( + onTopicClick = navigator::navigateToTopic, + onShowSnackbar = { message, action -> + snackbarHostState.showSnackbar( + message = message, + actionLabel = action, + duration = Short, + ) == ActionPerformed + }, + ) + } +} + +// TODO: Why is this here? +val LocalSnackbarHostState = compositionLocalOf { + error("SnackbarHostState state should be initialized at runtime") +} diff --git a/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt b/feature/bookmarks/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModelTest.kt similarity index 97% rename from feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt rename to feature/bookmarks/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModelTest.kt index aa42adae2..66ce0744f 100644 --- a/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt +++ b/feature/bookmarks/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.bookmarks +package com.google.samples.apps.nowinandroid.feature.bookmarks.impl import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData @@ -23,6 +23,7 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success +import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.BookmarksViewModel import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt deleted file mode 100644 index ea8d525ab..000000000 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.samples.apps.nowinandroid.feature.bookmarks.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute -import kotlinx.serialization.Serializable - -@Serializable object BookmarksRoute - -fun NavController.navigateToBookmarks(navOptions: NavOptions) = - navigate(route = BookmarksRoute, navOptions) - -fun NavGraphBuilder.bookmarksScreen( - onTopicClick: (String) -> Unit, - onShowSnackbar: suspend (String, String?) -> Boolean, -) { - composable { - BookmarksRoute(onTopicClick, onShowSnackbar) - } -} diff --git a/feature/bookmarks/src/main/res/drawable/feature_bookmarks_img_empty_bookmarks.xml b/feature/bookmarks/src/main/res/drawable/feature_bookmarks_img_empty_bookmarks.xml deleted file mode 100644 index 64bbfbd23..000000000 --- a/feature/bookmarks/src/main/res/drawable/feature_bookmarks_img_empty_bookmarks.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/feature/foryou/README.md b/feature/foryou/README.md deleted file mode 100644 index 0f08cb827..000000000 --- a/feature/foryou/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# :feature:foryou module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_feature_foryou.svg) diff --git a/feature/search/.gitignore b/feature/foryou/api/.gitignore similarity index 100% rename from feature/search/.gitignore rename to feature/foryou/api/.gitignore diff --git a/feature/foryou/api/README.md b/feature/foryou/api/README.md new file mode 100644 index 000000000..81223ecac --- /dev/null +++ b/feature/foryou/api/README.md @@ -0,0 +1,57 @@ +# `:feature:foryou:api` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:foryou + direction TB + :feature:foryou:api[api]:::android-library + end + end + subgraph :core + direction TB + :core:navigation[navigation]:::android-library + end + + :feature:foryou:api --> :core:navigation + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/foryou/api/build.gradle.kts b/feature/foryou/api/build.gradle.kts new file mode 100644 index 000000000..99ac324a2 --- /dev/null +++ b/feature/foryou/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.feature.api) +} + +android { + namespace = "com.google.samples.apps.nowinandroid.feature.foryou.api" +} + +dependencies { + api(projects.core.navigation) +} diff --git a/feature/foryou/src/main/AndroidManifest.xml b/feature/foryou/api/src/main/AndroidManifest.xml similarity index 100% rename from feature/foryou/src/main/AndroidManifest.xml rename to feature/foryou/api/src/main/AndroidManifest.xml diff --git a/feature/foryou/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/api/navigation/ForYouNavKey.kt b/feature/foryou/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/api/navigation/ForYouNavKey.kt new file mode 100644 index 000000000..d61ae5a20 --- /dev/null +++ b/feature/foryou/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/api/navigation/ForYouNavKey.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.foryou.api.navigation + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +object ForYouNavKey : NavKey diff --git a/feature/foryou/src/main/res/drawable/feature_foryou_ic_icon_placeholder.xml b/feature/foryou/api/src/main/res/drawable/feature_foryou_api_ic_icon_placeholder.xml similarity index 100% rename from feature/foryou/src/main/res/drawable/feature_foryou_ic_icon_placeholder.xml rename to feature/foryou/api/src/main/res/drawable/feature_foryou_api_ic_icon_placeholder.xml diff --git a/feature/foryou/src/main/res/values/strings.xml b/feature/foryou/api/src/main/res/values/strings.xml similarity index 56% rename from feature/foryou/src/main/res/values/strings.xml rename to feature/foryou/api/src/main/res/values/strings.xml index 166749664..f0595944f 100644 --- a/feature/foryou/src/main/res/values/strings.xml +++ b/feature/foryou/api/src/main/res/values/strings.xml @@ -15,11 +15,10 @@ limitations under the License. --> - For you - Done - Loading for you… - Navigate up - What are you interested in? - Updates from topics you follow will appear here. Follow some things to get started. - - + For you + Done + Loading for you… + Navigate up + What are you interested in? + Updates from topics you follow will appear here. Follow some things to get started. + \ No newline at end of file diff --git a/feature/settings/.gitignore b/feature/foryou/impl/.gitignore similarity index 100% rename from feature/settings/.gitignore rename to feature/foryou/impl/.gitignore diff --git a/feature/foryou/impl/README.md b/feature/foryou/impl/README.md new file mode 100644 index 000000000..8c12460f4 --- /dev/null +++ b/feature/foryou/impl/README.md @@ -0,0 +1,102 @@ +# `:feature:foryou:impl` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:foryou + direction TB + :feature:foryou:api[api]:::android-library + :feature:foryou:impl[impl]:::android-library + end + subgraph :feature:topic + direction TB + :feature:topic:api[api]:::android-library + end + end + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:designsystem[designsystem]:::android-library + :core:domain[domain]:::android-library + :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + :core:ui[ui]:::android-library + end + + :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:foryou:api --> :core:navigation + :feature:foryou:impl -.-> :core:designsystem + :feature:foryou:impl -.-> :core:domain + :feature:foryou:impl -.-> :core:notifications + :feature:foryou:impl -.-> :core:ui + :feature:foryou:impl -.-> :feature:foryou:api + :feature:foryou:impl -.-> :feature:topic:api + :feature:topic:api -.-> :core:designsystem + :feature:topic:api --> :core:navigation + :feature:topic:api -.-> :core:ui + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/foryou/build.gradle.kts b/feature/foryou/impl/build.gradle.kts similarity index 82% rename from feature/foryou/build.gradle.kts rename to feature/foryou/impl/build.gradle.kts index 59f6844cf..7964f8c4f 100644 --- a/feature/foryou/build.gradle.kts +++ b/feature/foryou/impl/build.gradle.kts @@ -15,21 +15,23 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) + alias(libs.plugins.nowinandroid.android.feature.impl) alias(libs.plugins.nowinandroid.android.library.compose) - alias(libs.plugins.nowinandroid.android.library.jacoco) alias(libs.plugins.roborazzi) } android { - namespace = "com.google.samples.apps.nowinandroid.feature.foryou" + namespace = "com.google.samples.apps.nowinandroid.feature.foryou.impl" + testOptions.unitTests.isIncludeAndroidResources = true } dependencies { implementation(libs.accompanist.permissions) - implementation(projects.core.data) implementation(projects.core.domain) implementation(projects.core.notifications) + implementation(projects.feature.foryou.api) + implementation(projects.feature.topic.api) + implementation(libs.androidx.activity.compose) testImplementation(libs.hilt.android.testing) testImplementation(libs.robolectric) @@ -38,4 +40,4 @@ dependencies { androidTestImplementation(libs.bundles.androidx.compose.ui.test) androidTestImplementation(projects.core.testing) -} +} \ No newline at end of file diff --git a/feature/foryou/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt b/feature/foryou/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenTest.kt similarity index 95% rename from feature/foryou/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt rename to feature/foryou/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenTest.kt index c3ec5c560..fc4fc0241 100644 --- a/feature/foryou/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt +++ b/feature/foryou/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Box @@ -32,6 +32,7 @@ import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPer import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState +import com.google.samples.apps.nowinandroid.feature.foryou.api.R import org.junit.Rule import org.junit.Test @@ -45,7 +46,7 @@ class ForYouScreenTest { private val doneButtonMatcher by lazy { hasText( - composeTestRule.activity.resources.getString(R.string.feature_foryou_done), + composeTestRule.activity.resources.getString(R.string.feature_foryou_api_done), ) } @@ -70,7 +71,7 @@ class ForYouScreenTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(R.string.feature_foryou_loading), + composeTestRule.activity.resources.getString(R.string.feature_foryou_api_loading), ) .assertExists() } @@ -96,7 +97,7 @@ class ForYouScreenTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(R.string.feature_foryou_loading), + composeTestRule.activity.resources.getString(R.string.feature_foryou_api_loading), ) .assertExists() } @@ -200,7 +201,9 @@ class ForYouScreenTest { ForYouScreen( isSyncing = false, onboardingUiState = - OnboardingUiState.Shown(topics = followableTopicTestData), + OnboardingUiState.Shown( + topics = followableTopicTestData, + ), feedState = NewsFeedUiState.Loading, deepLinkedUserNewsResource = null, onTopicCheckedChanged = { _, _ -> }, @@ -215,7 +218,7 @@ class ForYouScreenTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(R.string.feature_foryou_loading), + composeTestRule.activity.resources.getString(R.string.feature_foryou_api_loading), ) .assertExists() } @@ -241,7 +244,7 @@ class ForYouScreenTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(R.string.feature_foryou_loading), + composeTestRule.activity.resources.getString(R.string.feature_foryou_api_loading), ) .assertExists() } diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreen.kt similarity index 98% rename from feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt rename to feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreen.kt index 1a3325996..0ae916db3 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import android.net.Uri import android.os.Build.VERSION @@ -80,7 +80,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus.Denied @@ -103,9 +103,10 @@ import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider import com.google.samples.apps.nowinandroid.core.ui.launchCustomChromeTab import com.google.samples.apps.nowinandroid.core.ui.newsFeed +import com.google.samples.apps.nowinandroid.feature.foryou.api.R @Composable -internal fun ForYouScreen( +fun ForYouScreen( onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, viewModel: ForYouViewModel = hiltViewModel(), @@ -215,7 +216,7 @@ internal fun ForYouScreen( targetOffsetY = { fullHeight -> -fullHeight }, ) + fadeOut(), ) { - val loadingContentDescription = stringResource(id = R.string.feature_foryou_loading) + val loadingContentDescription = stringResource(id = R.string.feature_foryou_api_loading) Box( modifier = Modifier .fillMaxWidth() @@ -270,7 +271,7 @@ private fun LazyStaggeredGridScope.onboarding( item(span = StaggeredGridItemSpan.FullLine, contentType = "onboarding") { Column(modifier = interestsItemModifier) { Text( - text = stringResource(R.string.feature_foryou_onboarding_guidance_title), + text = stringResource(R.string.feature_foryou_api_onboarding_guidance_title), textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() @@ -278,7 +279,7 @@ private fun LazyStaggeredGridScope.onboarding( style = MaterialTheme.typography.titleMedium, ) Text( - text = stringResource(R.string.feature_foryou_onboarding_guidance_subtitle), + text = stringResource(R.string.feature_foryou_api_onboarding_guidance_subtitle), modifier = Modifier .fillMaxWidth() .padding(top = 8.dp, start = 24.dp, end = 24.dp), @@ -304,7 +305,7 @@ private fun LazyStaggeredGridScope.onboarding( .fillMaxWidth(), ) { Text( - text = stringResource(R.string.feature_foryou_done), + text = stringResource(R.string.feature_foryou_api_done), ) } } @@ -433,7 +434,7 @@ fun TopicIcon( modifier: Modifier = Modifier, ) { DynamicAsyncImage( - placeholder = painterResource(R.drawable.feature_foryou_ic_icon_placeholder), + placeholder = painterResource(R.drawable.feature_foryou_api_ic_icon_placeholder), imageUrl = imageUrl, // decorative contentDescription = null, diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModel.kt similarity index 98% rename from feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt rename to feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModel.kt index 4b6cd39c9..c54551c0b 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/OnboardingUiState.kt similarity index 95% rename from feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt rename to feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/OnboardingUiState.kt index 70634b4c5..d31749bb5 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/OnboardingUiState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic diff --git a/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt new file mode 100644 index 000000000..4bdc7368c --- /dev/null +++ b/feature/foryou/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/navigation/ForYouEntryProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation + +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey +import com.google.samples.apps.nowinandroid.feature.foryou.impl.ForYouScreen +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic + +fun EntryProviderScope.forYouEntry(navigator: Navigator) { + entry { + ForYouScreen( + onTopicClick = navigator::navigateToTopic, + ) + } +} diff --git a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenScreenshotTests.kt b/feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenScreenshotTests.kt similarity index 94% rename from feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenScreenshotTests.kt rename to feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenScreenshotTests.kt index 29fc6f536..d0d73860e 100644 --- a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenScreenshotTests.kt +++ b/feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouScreenScreenshotTests.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable @@ -31,9 +31,8 @@ import com.google.samples.apps.nowinandroid.core.testing.util.captureMultiDevice import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider -import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.Loading -import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.NotShown -import com.google.samples.apps.nowinandroid.feature.foryou.OnboardingUiState.Shown +import com.google.samples.apps.nowinandroid.feature.foryou.impl.OnboardingUiState.NotShown +import com.google.samples.apps.nowinandroid.feature.foryou.impl.OnboardingUiState.Shown import dagger.hilt.android.testing.HiltTestApplication import org.hamcrest.Matchers import org.junit.Before @@ -97,7 +96,7 @@ class ForYouScreenScreenshotTests { NiaTheme { ForYouScreen( isSyncing = false, - onboardingUiState = Loading, + onboardingUiState = OnboardingUiState.Loading, feedState = NewsFeedUiState.Loading, onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, @@ -194,7 +193,7 @@ class ForYouScreenScreenshotTests { NiaTheme { ForYouScreen( isSyncing = true, - onboardingUiState = Loading, + onboardingUiState = OnboardingUiState.Loading, feedState = Success( feed = userNewsResources, ), diff --git a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModelTest.kt similarity index 99% rename from feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt rename to feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModelTest.kt index 812544c0c..5008b484c 100644 --- a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature/foryou/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/impl/ForYouViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.foryou +package com.google.samples.apps.nowinandroid.feature.foryou.impl import androidx.lifecycle.SavedStateHandle import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_foldable.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_foldable.png new file mode 100644 index 000000000..51ffcce75 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_foldable.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_phone.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_phone.png new file mode 100644 index 000000000..e6c3b6c5a Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_phone.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_tablet.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_tablet.png new file mode 100644 index 000000000..eebc53e33 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenLoading_tablet.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png new file mode 100644 index 000000000..d7e5af23b Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png new file mode 100644 index 000000000..4bc0969af Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png new file mode 100644 index 000000000..e52802664 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png new file mode 100644 index 000000000..3cecfec33 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png new file mode 100644 index 000000000..cbe01938b Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png new file mode 100644 index 000000000..2ef02d5ce Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png new file mode 100644 index 000000000..849fd488b Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_foldable.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_foldable.png new file mode 100644 index 000000000..6ea161e10 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_foldable.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone.png new file mode 100644 index 000000000..eb7de8009 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone_dark.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone_dark.png new file mode 100644 index 000000000..693db55ff Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_phone_dark.png differ diff --git a/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_tablet.png b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_tablet.png new file mode 100644 index 000000000..ffa2b2262 Binary files /dev/null and b/feature/foryou/impl/src/test/screenshots/ForYouScreenTopicSelection_tablet.png differ diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt deleted file mode 100644 index b77ce72a0..000000000 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.samples.apps.nowinandroid.feature.foryou.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import androidx.navigation.compose.navigation -import androidx.navigation.navDeepLink -import com.google.samples.apps.nowinandroid.core.notifications.DEEP_LINK_URI_PATTERN -import com.google.samples.apps.nowinandroid.feature.foryou.ForYouScreen -import kotlinx.serialization.Serializable - -@Serializable data object ForYouRoute // route to ForYou screen - -@Serializable data object ForYouBaseRoute // route to base navigation graph - -fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(route = ForYouRoute, navOptions) - -/** - * The ForYou section of the app. It can also display information about topics. - * This should be supplied from a separate module. - * - * @param onTopicClick - Called when a topic is clicked, contains the ID of the topic - * @param topicDestination - Destination for topic content - */ -fun NavGraphBuilder.forYouSection( - onTopicClick: (String) -> Unit, - topicDestination: NavGraphBuilder.() -> Unit, -) { - navigation(startDestination = ForYouRoute) { - composable( - deepLinks = listOf( - navDeepLink { - /** - * This destination has a deep link that enables a specific news resource to be - * opened from a notification (@see SystemTrayNotifier for more). The news resource - * ID is sent in the URI rather than being modelled in the route type because it's - * transient data (stored in SavedStateHandle) that is cleared after the user has - * opened the news resource. - */ - uriPattern = DEEP_LINK_URI_PATTERN - }, - ), - ) { - ForYouScreen(onTopicClick) - } - topicDestination() - } -} diff --git a/feature/foryou/src/test/screenshots/ForYouScreenLoading_foldable.png b/feature/foryou/src/test/screenshots/ForYouScreenLoading_foldable.png deleted file mode 100644 index 538c9032c..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenLoading_foldable.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenLoading_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenLoading_phone.png deleted file mode 100644 index afd7dd708..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenLoading_phone.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenLoading_tablet.png b/feature/foryou/src/test/screenshots/ForYouScreenLoading_tablet.png deleted file mode 100644 index 1c197e8f0..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenLoading_tablet.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png index 3990aee3f..e4ecf6d95 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png deleted file mode 100644 index a86a8232f..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png deleted file mode 100644 index d500394ef..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png index 3b735462f..53891bdc1 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png index bc9c7dbec..ba669e8f8 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png deleted file mode 100644 index 1498f9b7b..000000000 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png and /dev/null differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png index 2b16ff76f..e92ae0481 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_foldable.png b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_foldable.png index 7ea92f8a8..3ae1bee94 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_foldable.png and b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_foldable.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_phone.png index b2faa3a28..0938678b2 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_phone.png and b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_phone.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_phone_dark.png b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_phone_dark.png index 4c30995b2..084d031a5 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_phone_dark.png and b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_phone_dark.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_tablet.png b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_tablet.png index 05f1ba1ed..35f59e54c 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_tablet.png and b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_tablet.png differ diff --git a/feature/interests/README.md b/feature/interests/README.md deleted file mode 100644 index 90a4fbc9c..000000000 --- a/feature/interests/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# :feature:interests module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_feature_interests.svg) diff --git a/feature/topic/.gitignore b/feature/interests/api/.gitignore similarity index 100% rename from feature/topic/.gitignore rename to feature/interests/api/.gitignore diff --git a/feature/interests/api/README.md b/feature/interests/api/README.md new file mode 100644 index 000000000..ef580df7c --- /dev/null +++ b/feature/interests/api/README.md @@ -0,0 +1,57 @@ +# `:feature:interests:api` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:interests + direction TB + :feature:interests:api[api]:::android-library + end + end + subgraph :core + direction TB + :core:navigation[navigation]:::android-library + end + + :feature:interests:api --> :core:navigation + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/interests/build.gradle.kts b/feature/interests/api/build.gradle.kts similarity index 58% rename from feature/interests/build.gradle.kts rename to feature/interests/api/build.gradle.kts index 2b84b135f..7a2dfd65e 100644 --- a/feature/interests/build.gradle.kts +++ b/feature/interests/api/build.gradle.kts @@ -15,21 +15,9 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) - alias(libs.plugins.nowinandroid.android.library.compose) - alias(libs.plugins.nowinandroid.android.library.jacoco) + alias(libs.plugins.nowinandroid.android.feature.api) } -android { - namespace = "com.google.samples.apps.nowinandroid.feature.interests" -} - -dependencies { - implementation(projects.core.data) - implementation(projects.core.domain) - testImplementation(projects.core.testing) - testImplementation(libs.robolectric) - - androidTestImplementation(libs.bundles.androidx.compose.ui.test) - androidTestImplementation(projects.core.testing) -} +android { + namespace = "com.google.samples.apps.nowinandroid.feature.interests.api" +} \ No newline at end of file diff --git a/feature/interests/src/main/AndroidManifest.xml b/feature/interests/api/src/main/AndroidManifest.xml similarity index 100% rename from feature/interests/src/main/AndroidManifest.xml rename to feature/interests/api/src/main/AndroidManifest.xml diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt b/feature/interests/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/api/navigation/InterestsNavKey.kt similarity index 67% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt rename to feature/interests/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/api/navigation/InterestsNavKey.kt index d83e4a9b2..cd6c631a2 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt +++ b/feature/interests/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/api/navigation/InterestsNavKey.kt @@ -14,20 +14,13 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.interests.navigation +package com.google.samples.apps.nowinandroid.feature.interests.api.navigation -import androidx.navigation.NavController -import androidx.navigation.NavOptions +import androidx.navigation3.runtime.NavKey import kotlinx.serialization.Serializable -@Serializable data class InterestsRoute( +@Serializable +data class InterestsNavKey( // The ID of the topic which will be initially selected at this destination val initialTopicId: String? = null, -) - -fun NavController.navigateToInterests( - initialTopicId: String? = null, - navOptions: NavOptions? = null, -) { - navigate(route = InterestsRoute(initialTopicId), navOptions) -} +) : NavKey diff --git a/feature/topic/src/main/res/drawable/feature_topic_ic_topic_placeholder.xml b/feature/interests/api/src/main/res/drawable/feature_interests_api_ic_detail_placeholder.xml similarity index 99% rename from feature/topic/src/main/res/drawable/feature_topic_ic_topic_placeholder.xml rename to feature/interests/api/src/main/res/drawable/feature_interests_api_ic_detail_placeholder.xml index 0518401da..2789b54e6 100644 --- a/feature/topic/src/main/res/drawable/feature_topic_ic_topic_placeholder.xml +++ b/feature/interests/api/src/main/res/drawable/feature_interests_api_ic_detail_placeholder.xml @@ -52,4 +52,4 @@ android:strokeColor="#8C4190" android:strokeLineCap="round" android:strokeWidth="2" /> - + \ No newline at end of file diff --git a/feature/interests/src/main/res/values/strings.xml b/feature/interests/api/src/main/res/values/strings.xml similarity index 69% rename from feature/interests/src/main/res/values/strings.xml rename to feature/interests/api/src/main/res/values/strings.xml index 8d5322859..b02d91bcc 100644 --- a/feature/interests/src/main/res/values/strings.xml +++ b/feature/interests/api/src/main/res/values/strings.xml @@ -15,7 +15,8 @@ limitations under the License. --> - Interests - Loading data - "No available data" + Interests + Loading data + "No available data" + Select an Interest diff --git a/feature/interests/impl/.gitignore b/feature/interests/impl/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/interests/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/interests/impl/README.md b/feature/interests/impl/README.md new file mode 100644 index 000000000..722f4ef41 --- /dev/null +++ b/feature/interests/impl/README.md @@ -0,0 +1,3 @@ +# :feature:interests:impl module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_feature_interests_impl.svg) diff --git a/feature/interests/impl/build.gradle.kts b/feature/interests/impl/build.gradle.kts new file mode 100644 index 000000000..fedc7fba6 --- /dev/null +++ b/feature/interests/impl/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.feature.impl) + alias(libs.plugins.nowinandroid.android.library.compose) + alias(libs.plugins.nowinandroid.android.library.jacoco) +} +android { + namespace = "com.google.samples.apps.nowinandroid.feature.interests.impl" + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(projects.core.domain) + implementation(projects.feature.topic.api) + implementation(projects.feature.interests.api) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.androidx.compose.material3.adaptive.layout) + implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.navigation3) + + testImplementation(projects.core.testing) + testImplementation(projects.core.dataTest) + testImplementation(libs.robolectric) + testImplementation(libs.androidx.compose.ui.test) + testImplementation(libs.androidx.test.espresso.core) + testImplementation(libs.hilt.android.testing) + testImplementation(projects.uiTestHiltManifest) + testImplementation(projects.feature.topic.impl) + testImplementation(libs.androidx.navigation.testing) + + androidTestImplementation(libs.bundles.androidx.compose.ui.test) + androidTestImplementation(projects.core.testing) +} diff --git a/feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt b/feature/interests/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreenTest.kt similarity index 92% rename from feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt rename to feature/interests/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreenTest.kt index a441f5a9d..8a10a478b 100644 --- a/feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt +++ b/feature/interests/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreenTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.interests +package com.google.samples.apps.nowinandroid.feature.interests.impl import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable @@ -25,13 +25,11 @@ import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData -import com.google.samples.apps.nowinandroid.feature.interests.InterestsScreen -import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState import org.junit.Before import org.junit.Rule import org.junit.Test import com.google.samples.apps.nowinandroid.core.ui.R as CoreUiR -import com.google.samples.apps.nowinandroid.feature.interests.R as InterestsR +import com.google.samples.apps.nowinandroid.feature.interests.api.R as InterestsR /** * UI test for checking the correct behaviour of the Interests screen; @@ -51,8 +49,8 @@ class InterestsScreenTest { @Before fun setup() { composeTestRule.activity.apply { - interestsLoading = getString(InterestsR.string.feature_interests_loading) - interestsEmptyHeader = getString(InterestsR.string.feature_interests_empty_header) + interestsLoading = getString(InterestsR.string.feature_interests_api_loading) + interestsEmptyHeader = getString(InterestsR.string.feature_interests_api_empty_header) interestsTopicCardFollowButton = getString(CoreUiR.string.core_ui_interests_card_follow_button_content_desc) interestsTopicCardUnfollowButton = diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicDetailPlaceholder.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsDetailPlaceholder.kt similarity index 86% rename from feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicDetailPlaceholder.kt rename to feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsDetailPlaceholder.kt index 627fb8fb3..2a03019d3 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicDetailPlaceholder.kt +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsDetailPlaceholder.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.topic +package com.google.samples.apps.nowinandroid.feature.interests.impl import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -33,9 +33,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme +import com.google.samples.apps.nowinandroid.feature.interests.api.R @Composable -fun TopicDetailPlaceholder(modifier: Modifier = Modifier) { +fun InterestsDetailPlaceholder(modifier: Modifier = Modifier) { Card( modifier = modifier, colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), @@ -50,12 +51,12 @@ fun TopicDetailPlaceholder(modifier: Modifier = Modifier) { ), ) { Icon( - painter = painterResource(id = R.drawable.feature_topic_ic_topic_placeholder), + painter = painterResource(id = R.drawable.feature_interests_api_ic_detail_placeholder), contentDescription = null, tint = MaterialTheme.colorScheme.primary, ) Text( - text = stringResource(id = R.string.feature_topic_select_an_interest), + text = stringResource(id = R.string.feature_interests_api_select_an_interest), style = MaterialTheme.typography.titleLarge, ) } @@ -66,6 +67,6 @@ fun TopicDetailPlaceholder(modifier: Modifier = Modifier) { @Composable fun TopicDetailPlaceholderPreview() { NiaTheme { - TopicDetailPlaceholder() + InterestsDetailPlaceholder() } } diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreen.kt similarity index 91% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt rename to feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreen.kt index 9b18ac89b..689b5bbf0 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.interests +package com.google.samples.apps.nowinandroid.feature.interests.impl import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text @@ -24,7 +24,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel @@ -33,13 +32,14 @@ import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.FollowableTopicPreviewParameterProvider import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent +import com.google.samples.apps.nowinandroid.feature.interests.api.R @Composable -fun InterestsRoute( +fun InterestsScreen( onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, + viewModel: InterestsViewModel, shouldHighlightSelectedTopic: Boolean = false, - viewModel: InterestsViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -47,6 +47,7 @@ fun InterestsRoute( uiState = uiState, followTopic = viewModel::followTopic, onTopicClick = { + // TODO: this violates SSOT, events should go through the ViewModel viewModel.onTopicClick(it) onTopicClick(it) }, @@ -70,7 +71,7 @@ internal fun InterestsScreen( when (uiState) { InterestsUiState.Loading -> NiaLoadingWheel( - contentDesc = stringResource(id = R.string.feature_interests_loading), + contentDesc = stringResource(id = R.string.feature_interests_api_loading), ) is InterestsUiState.Interests -> @@ -90,7 +91,7 @@ internal fun InterestsScreen( @Composable private fun InterestsEmptyScreen() { - Text(text = stringResource(id = R.string.feature_interests_empty_header)) + Text(text = stringResource(id = R.string.feature_interests_api_empty_header)) } @DevicePreviews diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsViewModel.kt similarity index 74% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt rename to feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsViewModel.kt index 67cc8884f..f79d79d09 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/InterestsViewModel.kt @@ -14,39 +14,43 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.interests +package com.google.samples.apps.nowinandroid.feature.interests.impl import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.navigation.toRoute import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.domain.TopicSortField import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic -import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class InterestsViewModel @Inject constructor( +@HiltViewModel(assistedFactory = InterestsViewModel.Factory::class) +class InterestsViewModel @AssistedInject constructor( private val savedStateHandle: SavedStateHandle, val userDataRepository: UserDataRepository, getFollowableTopics: GetFollowableTopicsUseCase, + // TODO: see comment below + @Assisted val key: InterestsNavKey, ) : ViewModel() { + // TODO: this should no longer be necessary, the currently selected topic should be + // available through the navigation state // Key used to save and retrieve the currently selected topic id from saved state. private val selectedTopicIdKey = "selectedTopicIdKey" - private val interestsRoute: InterestsRoute = savedStateHandle.toRoute() private val selectedTopicId = savedStateHandle.getStateFlow( key = selectedTopicIdKey, - initialValue = interestsRoute.initialTopicId, + initialValue = key.initialTopicId, ) val uiState: StateFlow = combine( @@ -66,8 +70,15 @@ class InterestsViewModel @Inject constructor( } fun onTopicClick(topicId: String?) { + // TODO: This should modify the navigation state directly rather than just updating the + // savedStateHandle savedStateHandle[selectedTopicIdKey] = topicId } + + @AssistedFactory + interface Factory { + fun create(key: InterestsNavKey): InterestsViewModel + } } sealed interface InterestsUiState { diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/TabContent.kt similarity index 96% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt rename to feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/TabContent.kt index 133c2bedd..d8a09c8f0 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/TabContent.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.interests +package com.google.samples.apps.nowinandroid.feature.interests.impl import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box @@ -59,7 +59,7 @@ fun TopicsTabContent( LazyColumn( modifier = Modifier .padding(horizontal = 24.dp) - .testTag("interests:topics"), + .testTag(LIST_PANE_TEST_TAG), contentPadding = PaddingValues(vertical = 16.dp), state = scrollableState, ) { @@ -103,3 +103,5 @@ fun TopicsTabContent( ) } } + +val LIST_PANE_TEST_TAG = "interests:topics" diff --git a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt new file mode 100644 index 000000000..b92f462eb --- /dev/null +++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/navigation/InterestsEntryProvider.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.interests.impl.navigation + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsDetailPlaceholder +import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsScreen +import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsViewModel +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +fun EntryProviderScope.interestsEntry(navigator: Navigator) { + entry( + metadata = ListDetailSceneStrategy.listPane { + InterestsDetailPlaceholder() + }, + ) { key -> + val viewModel = hiltViewModel { + it.create(key) + } + InterestsScreen( + // TODO: This event should either be provided by the ViewModel or by the navigator, not both + onTopicClick = navigator::navigateToTopic, + + // TODO: This should be dynamically calculated based on the rendering scene + // See https://github.com/android/nav3-recipes/commit/488f4811791ca3ed7192f4fe3c86e7371b32ebdc#diff-374e02026cdd2f68057dd940f203dc4ba7319930b33e9555c61af7e072211cabR89 + shouldHighlightSelectedTopic = false, + viewModel = viewModel, + ) + } +} diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/InterestsListDetailScreenTest.kt b/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsListDetailScreenTest.kt similarity index 68% rename from app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/InterestsListDetailScreenTest.kt rename to feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsListDetailScreenTest.kt index 1062c7e56..0975216d1 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/InterestsListDetailScreenTest.kt +++ b/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsListDetailScreenTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Android Open Source Project + * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,14 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.ui +@file:OptIn(ExperimentalMaterial3AdaptiveApi::class) + +package com.google.samples.apps.nowinandroid.interests.impl -import androidx.activity.compose.BackHandler import androidx.annotation.StringRes +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy +import androidx.compose.runtime.Composable import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.junit4.AndroidComposeTestRule @@ -25,11 +29,20 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay import androidx.test.espresso.Espresso import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.Topic -import com.google.samples.apps.nowinandroid.ui.interests2pane.InterestsListDetailScreen +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.core.navigation.rememberNavigationState +import com.google.samples.apps.nowinandroid.core.navigation.toEntries +import com.google.samples.apps.nowinandroid.feature.interests.api.R +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import com.google.samples.apps.nowinandroid.feature.interests.impl.LIST_PANE_TEST_TAG +import com.google.samples.apps.nowinandroid.feature.interests.impl.navigation.interestsEntry +import com.google.samples.apps.nowinandroid.feature.topic.impl.navigation.topicEntry import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -43,16 +56,15 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import javax.inject.Inject +import kotlin.getValue import kotlin.properties.ReadOnlyProperty -import kotlin.test.assertTrue -import com.google.samples.apps.nowinandroid.feature.topic.R as FeatureTopicR private const val EXPANDED_WIDTH = "w1200dp-h840dp" private const val COMPACT_WIDTH = "w412dp-h915dp" @HiltAndroidTest @RunWith(RobolectricTestRunner::class) -@Config(application = HiltTestApplication::class) +@Config(application = HiltTestApplication::class, sdk = [35]) class InterestsListDetailScreenTest { @get:Rule(order = 0) @@ -70,8 +82,7 @@ class InterestsListDetailScreenTest { } // The strings used for matching in these tests. - private val placeholderText by composeTestRule.stringResource(FeatureTopicR.string.feature_topic_select_an_interest) - private val listPaneTag = "interests:topics" + private val placeholderText by composeTestRule.stringResource(R.string.feature_interests_api_select_an_interest) private val Topic.testTag get() = "topic:${this.id}" @@ -87,26 +98,48 @@ class InterestsListDetailScreenTest { composeTestRule.apply { setContent { NiaTheme { - InterestsListDetailScreen() + TestNavDisplay() } } - - onNodeWithTag(listPaneTag).assertIsDisplayed() + onNodeWithTag(LIST_PANE_TEST_TAG).assertIsDisplayed() onNodeWithText(placeholderText).assertIsDisplayed() } } + @Composable + private fun TestNavDisplay() { + val startKey = InterestsNavKey(null) + + val navigationState = rememberNavigationState( + startKey = startKey, + topLevelKeys = setOf(startKey), + ) + + val navigator = Navigator(navigationState) + + val entryProvider = entryProvider { + interestsEntry(navigator) + topicEntry(navigator) + } + + NavDisplay( + entries = navigationState.toEntries(entryProvider), + onBack = { navigator.goBack() }, + sceneStrategy = rememberListDetailSceneStrategy(), + ) + } + @Test @Config(qualifiers = COMPACT_WIDTH) fun compactWidth_initialState_showsListPane() { composeTestRule.apply { setContent { NiaTheme { - InterestsListDetailScreen() + TestNavDisplay() } } - onNodeWithTag(listPaneTag).assertIsDisplayed() + onNodeWithTag(LIST_PANE_TEST_TAG).assertIsDisplayed() onNodeWithText(placeholderText).assertIsNotDisplayed() } } @@ -117,14 +150,14 @@ class InterestsListDetailScreenTest { composeTestRule.apply { setContent { NiaTheme { - InterestsListDetailScreen() + TestNavDisplay() } } - val firstTopic = getTopics().first() onNodeWithText(firstTopic.name).performClick() + waitForIdle() - onNodeWithTag(listPaneTag).assertIsDisplayed() + onNodeWithTag(LIST_PANE_TEST_TAG).assertIsDisplayed() onNodeWithText(placeholderText).assertIsNotDisplayed() onNodeWithTag(firstTopic.testTag).assertIsDisplayed() } @@ -136,52 +169,26 @@ class InterestsListDetailScreenTest { composeTestRule.apply { setContent { NiaTheme { - InterestsListDetailScreen() + TestNavDisplay() } } val firstTopic = getTopics().first() onNodeWithText(firstTopic.name).performClick() - onNodeWithTag(listPaneTag).assertIsNotDisplayed() + onNodeWithTag(LIST_PANE_TEST_TAG).assertIsNotDisplayed() onNodeWithText(placeholderText).assertIsNotDisplayed() onNodeWithTag(firstTopic.testTag).assertIsDisplayed() } } - @Test - @Config(qualifiers = EXPANDED_WIDTH) - fun expandedWidth_backPressFromTopicDetail_leavesInterests() { - var unhandledBackPress = false - composeTestRule.apply { - setContent { - NiaTheme { - // Back press should not be handled by the two pane layout, and thus - // "fall through" to this BackHandler. - BackHandler { - unhandledBackPress = true - } - InterestsListDetailScreen() - } - } - - val firstTopic = getTopics().first() - onNodeWithText(firstTopic.name).performClick() - - waitForIdle() - Espresso.pressBack() - - assertTrue(unhandledBackPress) - } - } - @Test @Config(qualifiers = COMPACT_WIDTH) fun compactWidth_backPressFromTopicDetail_showsListPane() { composeTestRule.apply { setContent { NiaTheme { - InterestsListDetailScreen() + TestNavDisplay() } } @@ -191,7 +198,7 @@ class InterestsListDetailScreenTest { waitForIdle() Espresso.pressBack() - onNodeWithTag(listPaneTag).assertIsDisplayed() + onNodeWithTag(LIST_PANE_TEST_TAG).assertIsDisplayed() onNodeWithText(placeholderText).assertIsNotDisplayed() onNodeWithTag(firstTopic.testTag).assertIsNotDisplayed() } diff --git a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt b/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsViewModelTest.kt similarity index 93% rename from feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt rename to feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsViewModelTest.kt index cdf21f325..4e964b52b 100644 --- a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt +++ b/feature/interests/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/impl/InterestsViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.interests +package com.google.samples.apps.nowinandroid.interests.impl import androidx.lifecycle.SavedStateHandle import androidx.navigation.testing.invoke @@ -24,9 +24,9 @@ import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule -import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState -import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel -import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsUiState +import com.google.samples.apps.nowinandroid.feature.interests.impl.InterestsViewModel import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -36,6 +36,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import kotlin.test.assertEquals /** @@ -49,6 +50,7 @@ import kotlin.test.assertEquals * See https://issuetracker.google.com/340966212. */ @RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) class InterestsViewModelTest { @get:Rule @@ -66,10 +68,11 @@ class InterestsViewModelTest { fun setup() { viewModel = InterestsViewModel( savedStateHandle = SavedStateHandle( - route = InterestsRoute(initialTopicId = testInputTopics[0].topic.id), + route = InterestsNavKey(initialTopicId = testInputTopics[0].topic.id), ), userDataRepository = userDataRepository, getFollowableTopics = getFollowableTopicsUseCase, + InterestsNavKey(initialTopicId = testInputTopics[0].topic.id), ) } diff --git a/feature/search/README.md b/feature/search/README.md deleted file mode 100644 index e205970f0..000000000 --- a/feature/search/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# :feature:search module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_feature_search.svg) diff --git a/feature/search/api/.gitignore b/feature/search/api/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/search/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/search/api/README.md b/feature/search/api/README.md new file mode 100644 index 000000000..a468f649d --- /dev/null +++ b/feature/search/api/README.md @@ -0,0 +1,84 @@ +# `:feature:search:api` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:search + direction TB + :feature:search:api[api]:::android-library + end + end + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:domain[domain]:::android-library + :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + end + + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:domain --> :core:data + :core:domain --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :core:model + :feature:search:api -.-> :core:domain + :feature:search:api --> :core:navigation + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/search/api/build.gradle.kts b/feature/search/api/build.gradle.kts new file mode 100644 index 000000000..d7ea6fc5f --- /dev/null +++ b/feature/search/api/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.feature.api) +} + +android { + namespace = "com.google.samples.apps.nowinandroid.feature.search.api" +} + +dependencies { + implementation(projects.core.domain) +} + diff --git a/feature/search/src/main/AndroidManifest.xml b/feature/search/api/src/main/AndroidManifest.xml similarity index 100% rename from feature/search/src/main/AndroidManifest.xml rename to feature/search/api/src/main/AndroidManifest.xml diff --git a/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchNavKey.kt b/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchNavKey.kt new file mode 100644 index 000000000..9588a8404 --- /dev/null +++ b/feature/search/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/api/navigation/SearchNavKey.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.search.api.navigation + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +object SearchNavKey : NavKey diff --git a/feature/search/api/src/main/res/values/strings.xml b/feature/search/api/src/main/res/values/strings.xml new file mode 100644 index 000000000..d2d218493 --- /dev/null +++ b/feature/search/api/src/main/res/values/strings.xml @@ -0,0 +1,29 @@ + + + + Search + Clear search text + Sorry, there is no content found for your search \"%1$s\" + Sorry, we are still processing the search index. Please come back later + Try another search or explorer + Interests + to browse topics + Topics + Updates + Recent searches + Clear searches + diff --git a/feature/search/impl/.gitignore b/feature/search/impl/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/search/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/search/impl/README.md b/feature/search/impl/README.md new file mode 100644 index 000000000..04800005c --- /dev/null +++ b/feature/search/impl/README.md @@ -0,0 +1,108 @@ +# `:feature:search:impl` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :feature + direction TB + subgraph :feature:search + direction TB + :feature:search:api[api]:::android-library + :feature:search:impl[impl]:::android-library + end + subgraph :feature:interests + direction TB + :feature:interests:api[api]:::android-library + end + subgraph :feature:topic + direction TB + :feature:topic:api[api]:::android-library + end + end + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:designsystem[designsystem]:::android-library + :core:domain[domain]:::android-library + :core:model[model]:::jvm-library + :core:navigation[navigation]:::android-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + :core:ui[ui]:::android-library + end + + :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:interests:api --> :core:navigation + :feature:search:api -.-> :core:domain + :feature:search:api --> :core:navigation + :feature:search:impl -.-> :core:designsystem + :feature:search:impl -.-> :core:domain + :feature:search:impl -.-> :core:ui + :feature:search:impl -.-> :feature:interests:api + :feature:search:impl -.-> :feature:search:api + :feature:search:impl -.-> :feature:topic:api + :feature:topic:api -.-> :core:designsystem + :feature:topic:api --> :core:navigation + :feature:topic:api -.-> :core:ui + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/search/build.gradle.kts b/feature/search/impl/build.gradle.kts similarity index 82% rename from feature/search/build.gradle.kts rename to feature/search/impl/build.gradle.kts index 5bb659c35..8425b29f3 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/impl/build.gradle.kts @@ -15,22 +15,23 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) + alias(libs.plugins.nowinandroid.android.feature.impl) alias(libs.plugins.nowinandroid.android.library.compose) alias(libs.plugins.nowinandroid.android.library.jacoco) } android { - namespace = "com.google.samples.apps.nowinandroid.feature.search" + namespace = "com.google.samples.apps.nowinandroid.feature.search.impl" } dependencies { - implementation(projects.core.data) implementation(projects.core.domain) + implementation(projects.feature.interests.api) + implementation(projects.feature.search.api) + implementation(projects.feature.topic.api) testImplementation(projects.core.testing) androidTestImplementation(libs.bundles.androidx.compose.ui.test) androidTestImplementation(projects.core.testing) -} - +} \ No newline at end of file diff --git a/feature/search/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt b/feature/search/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreenTest.kt similarity index 93% rename from feature/search/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt rename to feature/search/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreenTest.kt index a9e2fa98f..3cb93530f 100644 --- a/feature/search/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt +++ b/feature/search/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreenTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.assertCountEquals @@ -36,6 +36,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData import com.google.samples.apps.nowinandroid.core.ui.R.string +import com.google.samples.apps.nowinandroid.feature.search.api.R import org.junit.Before import org.junit.Rule import org.junit.Test @@ -70,17 +71,17 @@ class SearchScreenTest { @Before fun setup() { composeTestRule.activity.apply { - clearSearchContentDesc = getString(R.string.feature_search_clear_search_text_content_desc) - clearRecentSearchesContentDesc = getString(R.string.feature_search_clear_recent_searches_content_desc) + clearSearchContentDesc = getString(R.string.feature_search_api_clear_search_text_content_desc) + clearRecentSearchesContentDesc = getString(R.string.feature_search_api_clear_recent_searches_content_desc) followButtonContentDesc = getString(string.core_ui_interests_card_follow_button_content_desc) unfollowButtonContentDesc = getString(string.core_ui_interests_card_unfollow_button_content_desc) - topicsString = getString(R.string.feature_search_topics) - updatesString = getString(R.string.feature_search_updates) - tryAnotherSearchString = getString(R.string.feature_search_try_another_search) + - " " + getString(R.string.feature_search_interests) + " " + getString(R.string.feature_search_to_browse_topics) - searchNotReadyString = getString(R.string.feature_search_not_ready) + topicsString = getString(R.string.feature_search_api_topics) + updatesString = getString(R.string.feature_search_api_updates) + tryAnotherSearchString = getString(R.string.feature_search_api_try_another_search) + + " " + getString(R.string.feature_search_api_interests) + " " + getString(R.string.feature_search_api_to_browse_topics) + searchNotReadyString = getString(R.string.feature_search_api_not_ready) } } diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/RecentSearchQueriesUiState.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/RecentSearchQueriesUiState.kt similarity index 93% rename from feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/RecentSearchQueriesUiState.kt rename to feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/RecentSearchQueriesUiState.kt index 8aa5bb3b8..5b8516664 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/RecentSearchQueriesUiState.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/RecentSearchQueriesUiState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchResultUiState.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchResultUiState.kt similarity index 96% rename from feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchResultUiState.kt rename to feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchResultUiState.kt index aaf7dba7d..7a6f37087 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchResultUiState.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchResultUiState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreen.kt similarity index 96% rename from feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt rename to feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreen.kt index b617f98a9..bb7164f2a 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation @@ -78,7 +78,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller @@ -93,10 +93,10 @@ import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success import com.google.samples.apps.nowinandroid.core.ui.R.string import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent import com.google.samples.apps.nowinandroid.core.ui.newsFeed -import com.google.samples.apps.nowinandroid.feature.search.R as searchR +import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR @Composable -internal fun SearchRoute( +internal fun SearchScreen( onBackClick: () -> Unit, onInterestsClick: () -> Unit, onTopicClick: (String) -> Unit, @@ -211,7 +211,7 @@ fun EmptySearchResultBody( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(horizontal = 48.dp), ) { - val message = stringResource(id = searchR.string.feature_search_result_not_found, searchQuery) + val message = stringResource(id = searchR.string.feature_search_api_result_not_found, searchQuery) val start = message.indexOf(searchQuery) Text( text = AnnotatedString( @@ -229,7 +229,7 @@ fun EmptySearchResultBody( modifier = Modifier.padding(vertical = 24.dp), ) val tryAnotherSearchString = buildAnnotatedString { - append(stringResource(id = searchR.string.feature_search_try_another_search)) + append(stringResource(id = searchR.string.feature_search_api_try_another_search)) append(" ") withLink( LinkAnnotation.Clickable( @@ -245,12 +245,12 @@ fun EmptySearchResultBody( fontWeight = FontWeight.Bold, ), ) { - append(stringResource(id = searchR.string.feature_search_interests)) + append(stringResource(id = searchR.string.feature_search_api_interests)) } } append(" ") - append(stringResource(id = searchR.string.feature_search_to_browse_topics)) + append(stringResource(id = searchR.string.feature_search_api_to_browse_topics)) } Text( text = tryAnotherSearchString, @@ -273,7 +273,7 @@ private fun SearchNotReadyBody() { modifier = Modifier.padding(horizontal = 48.dp), ) { Text( - text = stringResource(id = searchR.string.feature_search_not_ready), + text = stringResource(id = searchR.string.feature_search_api_not_ready), style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, modifier = Modifier.padding(vertical = 24.dp), @@ -314,7 +314,7 @@ private fun SearchResultBody( Text( text = buildAnnotatedString { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(stringResource(id = searchR.string.feature_search_topics)) + append(stringResource(id = searchR.string.feature_search_api_topics)) } }, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -350,7 +350,7 @@ private fun SearchResultBody( Text( text = buildAnnotatedString { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(stringResource(id = searchR.string.feature_search_updates)) + append(stringResource(id = searchR.string.feature_search_api_updates)) } }, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -402,7 +402,7 @@ private fun RecentSearchesBody( Text( text = buildAnnotatedString { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(stringResource(id = searchR.string.feature_search_recent_searches)) + append(stringResource(id = searchR.string.feature_search_api_recent_searches)) } }, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -417,7 +417,7 @@ private fun RecentSearchesBody( Icon( imageVector = NiaIcons.Close, contentDescription = stringResource( - id = searchR.string.feature_search_clear_recent_searches_content_desc, + id = searchR.string.feature_search_api_clear_recent_searches_content_desc, ), tint = MaterialTheme.colorScheme.onSurface, ) @@ -491,7 +491,7 @@ private fun SearchTextField( Icon( imageVector = NiaIcons.Search, contentDescription = stringResource( - id = searchR.string.feature_search_title, + id = searchR.string.feature_search_api_title, ), tint = MaterialTheme.colorScheme.onSurface, ) @@ -506,7 +506,7 @@ private fun SearchTextField( Icon( imageVector = NiaIcons.Close, contentDescription = stringResource( - id = searchR.string.feature_search_clear_search_text_content_desc, + id = searchR.string.feature_search_api_clear_search_text_content_desc, ), tint = MaterialTheme.colorScheme.onSurface, ) diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchUiStatePreviewParameterProvider.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchUiStatePreviewParameterProvider.kt similarity index 96% rename from feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchUiStatePreviewParameterProvider.kt rename to feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchUiStatePreviewParameterProvider.kt index 257d8b68e..1bda620c6 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchUiStatePreviewParameterProvider.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchUiStatePreviewParameterProvider.kt @@ -16,7 +16,7 @@ @file:Suppress("ktlint:standard:max-line-length") -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModel.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModel.kt similarity index 98% rename from feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModel.kt rename to feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModel.kt index 36947880e..13628de70 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModel.kt +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel diff --git a/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt new file mode 100644 index 000000000..4e3baf343 --- /dev/null +++ b/feature/search/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/navigation/SearchEntryProvider.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.search.impl.navigation + +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey +import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchNavKey +import com.google.samples.apps.nowinandroid.feature.search.impl.SearchScreen +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic + +fun EntryProviderScope.searchEntry(navigator: Navigator) { + entry { + SearchScreen( + onBackClick = { navigator.goBack() }, + onInterestsClick = { navigator.navigate(InterestsNavKey()) }, + onTopicClick = navigator::navigateToTopic, + ) + } +} diff --git a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt b/feature/search/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModelTest.kt similarity index 93% rename from feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt rename to feature/search/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModelTest.kt index 1b866cec2..a1f089b99 100644 --- a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt +++ b/feature/search/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/impl/SearchViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.search +package com.google.samples.apps.nowinandroid.feature.search.impl import androidx.lifecycle.SavedStateHandle import com.google.samples.apps.nowinandroid.core.analytics.NoOpAnalyticsHelper @@ -27,10 +27,10 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestSearchCo import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule -import com.google.samples.apps.nowinandroid.feature.search.RecentSearchQueriesUiState.Success -import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.EmptyQuery -import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.Loading -import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.SearchNotReady +import com.google.samples.apps.nowinandroid.feature.search.impl.RecentSearchQueriesUiState.Success +import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.EmptyQuery +import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.Loading +import com.google.samples.apps.nowinandroid.feature.search.impl.SearchResultUiState.SearchNotReady import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/navigation/SearchNavigation.kt b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/navigation/SearchNavigation.kt deleted file mode 100644 index 3b16e5f71..000000000 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/navigation/SearchNavigation.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.samples.apps.nowinandroid.feature.search.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import com.google.samples.apps.nowinandroid.feature.search.SearchRoute -import kotlinx.serialization.Serializable - -@Serializable data object SearchRoute - -fun NavController.navigateToSearch(navOptions: NavOptions? = null) = - navigate(SearchRoute, navOptions) - -fun NavGraphBuilder.searchScreen( - onBackClick: () -> Unit, - onInterestsClick: () -> Unit, - onTopicClick: (String) -> Unit, -) { - // TODO: Handle back stack for each top-level destination. At the moment each top-level - // destination may have own search screen's back stack. - composable { - SearchRoute( - onBackClick = onBackClick, - onInterestsClick = onInterestsClick, - onTopicClick = onTopicClick, - ) - } -} diff --git a/feature/search/src/main/res/values/strings.xml b/feature/search/src/main/res/values/strings.xml deleted file mode 100644 index e11576747..000000000 --- a/feature/search/src/main/res/values/strings.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - Search - Clear search text - Sorry, there is no content found for your search \"%1$s\" - Sorry, we are still processing the search index. Please come back later - Try another search or explorer - Interests - to browse topics - Topics - Updates - Recent searches - Clear searches - diff --git a/feature/settings/README.md b/feature/settings/README.md deleted file mode 100644 index 7a4df04fe..000000000 --- a/feature/settings/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# :feature:settings module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_feature_settings.svg) diff --git a/feature/settings/impl/.gitignore b/feature/settings/impl/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/settings/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/settings/impl/README.md b/feature/settings/impl/README.md new file mode 100644 index 000000000..88aadd960 --- /dev/null +++ b/feature/settings/impl/README.md @@ -0,0 +1,86 @@ +# `:feature:settings:api` + +## 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 + end + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:designsystem[designsystem]:::android-library + :core:model[model]:::jvm-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + :core:ui[ui]:::android-library + end + + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :core:model + :core:ui --> :core:analytics + :core:ui --> :core:designsystem + :core:ui --> :core:model + :feature:settings:impl -.-> :core:data + :feature:settings:impl -.-> :core:designsystem + :feature:settings:impl -.-> :core:ui + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
📋 Graph legend + +```mermaid +graph TB + application[application]:::android-application + feature[feature]:::android-feature + library[library]:::android-library + jvm[jvm]:::jvm-library + + application -.-> feature + library --> jvm + +classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; +classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; +classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; +classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; +classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; +``` + +
+ diff --git a/feature/settings/build.gradle.kts b/feature/settings/impl/build.gradle.kts similarity index 93% rename from feature/settings/build.gradle.kts rename to feature/settings/impl/build.gradle.kts index 15d65204d..d398e6103 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/impl/build.gradle.kts @@ -15,13 +15,13 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) + alias(libs.plugins.nowinandroid.android.feature.impl) alias(libs.plugins.nowinandroid.android.library.compose) alias(libs.plugins.nowinandroid.android.library.jacoco) } android { - namespace = "com.google.samples.apps.nowinandroid.feature.settings" + namespace = "com.google.samples.apps.nowinandroid.feature.settings.impl" } dependencies { diff --git a/feature/settings/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt b/feature/settings/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialogTest.kt similarity index 82% rename from feature/settings/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt rename to feature/settings/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialogTest.kt index 790b5964d..f0f69a424 100644 --- a/feature/settings/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialogTest.kt +++ b/feature/settings/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialogTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.settings +package com.google.samples.apps.nowinandroid.feature.settings.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.assertIsSelected @@ -23,8 +23,8 @@ import androidx.compose.ui.test.onNodeWithText import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Loading +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Success import org.junit.Rule import org.junit.Test @@ -48,7 +48,7 @@ class SettingsDialogTest { } composeTestRule - .onNodeWithText(getString(R.string.feature_settings_loading)) + .onNodeWithText(getString(R.string.feature_settings_impl_loading)) .assertExists() } @@ -71,17 +71,17 @@ class SettingsDialogTest { } // Check that all the possible settings are displayed. - composeTestRule.onNodeWithText(getString(R.string.feature_settings_brand_default)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_brand_android)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_brand_default)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_brand_android)).assertExists() composeTestRule.onNodeWithText( - getString(R.string.feature_settings_dark_mode_config_system_default), + getString(R.string.feature_settings_impl_dark_mode_config_system_default), ).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dark_mode_config_light)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dark_mode_config_dark)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dark_mode_config_light)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dark_mode_config_dark)).assertExists() // Check that the correct settings are selected. - composeTestRule.onNodeWithText(getString(R.string.feature_settings_brand_android)).assertIsSelected() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dark_mode_config_dark)).assertIsSelected() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_brand_android)).assertIsSelected() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dark_mode_config_dark)).assertIsSelected() } @Test @@ -103,12 +103,12 @@ class SettingsDialogTest { ) } - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_preference)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_yes)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_no)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_preference)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_yes)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_no)).assertExists() // Check that the correct default dynamic color setting is selected. - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_no)).assertIsSelected() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_no)).assertIsSelected() } @Test @@ -129,10 +129,10 @@ class SettingsDialogTest { ) } - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_preference)) + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_preference)) .assertDoesNotExist() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_yes)).assertDoesNotExist() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_no)).assertDoesNotExist() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_yes)).assertDoesNotExist() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_no)).assertDoesNotExist() } @Test @@ -153,10 +153,10 @@ class SettingsDialogTest { ) } - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_preference)) + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_preference)) .assertDoesNotExist() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_yes)).assertDoesNotExist() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_dynamic_color_no)).assertDoesNotExist() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_yes)).assertDoesNotExist() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_dynamic_color_no)).assertDoesNotExist() } @Test @@ -177,9 +177,9 @@ class SettingsDialogTest { ) } - composeTestRule.onNodeWithText(getString(R.string.feature_settings_privacy_policy)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_licenses)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_brand_guidelines)).assertExists() - composeTestRule.onNodeWithText(getString(R.string.feature_settings_feedback)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_privacy_policy)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_licenses)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_brand_guidelines)).assertExists() + composeTestRule.onNodeWithText(getString(R.string.feature_settings_impl_feedback)).assertExists() } } diff --git a/feature/settings/src/main/AndroidManifest.xml b/feature/settings/impl/src/main/AndroidManifest.xml similarity index 100% rename from feature/settings/src/main/AndroidManifest.xml rename to feature/settings/impl/src/main/AndroidManifest.xml diff --git a/feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt b/feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialog.kt similarity index 88% rename from feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt rename to feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialog.kt index ad7f30f43..b2758e286 100644 --- a/feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsDialog.kt +++ b/feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsDialog.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ @file:Suppress("ktlint:standard:max-line-length") -package com.google.samples.apps.nowinandroid.feature.settings +package com.google.samples.apps.nowinandroid.feature.settings.impl import android.content.Intent import androidx.compose.animation.AnimatedVisibility @@ -52,7 +52,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTextButton @@ -66,9 +66,9 @@ import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent -import com.google.samples.apps.nowinandroid.feature.settings.R.string -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success +import com.google.samples.apps.nowinandroid.feature.settings.impl.R.string +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Loading +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Success @Composable fun SettingsDialog( @@ -109,7 +109,7 @@ fun SettingsDialog( onDismissRequest = { onDismiss() }, title = { Text( - text = stringResource(string.feature_settings_title), + text = stringResource(string.feature_settings_impl_title), style = MaterialTheme.typography.titleLarge, ) }, @@ -119,7 +119,7 @@ fun SettingsDialog( when (settingsUiState) { Loading -> { Text( - text = stringResource(string.feature_settings_loading), + text = stringResource(string.feature_settings_impl_loading), modifier = Modifier.padding(vertical = 16.dp), ) } @@ -145,7 +145,7 @@ fun SettingsDialog( modifier = Modifier.padding(horizontal = 8.dp), ) { Text( - text = stringResource(string.feature_settings_dismiss_dialog_button_text), + text = stringResource(string.feature_settings_impl_dismiss_dialog_button_text), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, ) @@ -163,50 +163,50 @@ private fun ColumnScope.SettingsPanel( onChangeDynamicColorPreference: (useDynamicColor: Boolean) -> Unit, onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit, ) { - SettingsDialogSectionTitle(text = stringResource(string.feature_settings_theme)) + SettingsDialogSectionTitle(text = stringResource(string.feature_settings_impl_theme)) Column(Modifier.selectableGroup()) { SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_brand_default), + text = stringResource(string.feature_settings_impl_brand_default), selected = settings.brand == DEFAULT, onClick = { onChangeThemeBrand(DEFAULT) }, ) SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_brand_android), + text = stringResource(string.feature_settings_impl_brand_android), selected = settings.brand == ANDROID, onClick = { onChangeThemeBrand(ANDROID) }, ) } AnimatedVisibility(visible = settings.brand == DEFAULT && supportDynamicColor) { Column { - SettingsDialogSectionTitle(text = stringResource(string.feature_settings_dynamic_color_preference)) + SettingsDialogSectionTitle(text = stringResource(string.feature_settings_impl_dynamic_color_preference)) Column(Modifier.selectableGroup()) { SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_dynamic_color_yes), + text = stringResource(string.feature_settings_impl_dynamic_color_yes), selected = settings.useDynamicColor, onClick = { onChangeDynamicColorPreference(true) }, ) SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_dynamic_color_no), + text = stringResource(string.feature_settings_impl_dynamic_color_no), selected = !settings.useDynamicColor, onClick = { onChangeDynamicColorPreference(false) }, ) } } } - SettingsDialogSectionTitle(text = stringResource(string.feature_settings_dark_mode_preference)) + SettingsDialogSectionTitle(text = stringResource(string.feature_settings_impl_dark_mode_preference)) Column(Modifier.selectableGroup()) { SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_dark_mode_config_system_default), + text = stringResource(string.feature_settings_impl_dark_mode_config_system_default), selected = settings.darkThemeConfig == FOLLOW_SYSTEM, onClick = { onChangeDarkThemeConfig(FOLLOW_SYSTEM) }, ) SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_dark_mode_config_light), + text = stringResource(string.feature_settings_impl_dark_mode_config_light), selected = settings.darkThemeConfig == LIGHT, onClick = { onChangeDarkThemeConfig(LIGHT) }, ) SettingsDialogThemeChooserRow( - text = stringResource(string.feature_settings_dark_mode_config_dark), + text = stringResource(string.feature_settings_impl_dark_mode_config_dark), selected = settings.darkThemeConfig == DARK, onClick = { onChangeDarkThemeConfig(DARK) }, ) @@ -262,7 +262,7 @@ private fun LinksPanel() { NiaTextButton( onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) }, ) { - Text(text = stringResource(string.feature_settings_privacy_policy)) + Text(text = stringResource(string.feature_settings_impl_privacy_policy)) } val context = LocalContext.current NiaTextButton( @@ -270,17 +270,17 @@ private fun LinksPanel() { context.startActivity(Intent(context, OssLicensesMenuActivity::class.java)) }, ) { - Text(text = stringResource(string.feature_settings_licenses)) + Text(text = stringResource(string.feature_settings_impl_licenses)) } NiaTextButton( onClick = { uriHandler.openUri(BRAND_GUIDELINES_URL) }, ) { - Text(text = stringResource(string.feature_settings_brand_guidelines)) + Text(text = stringResource(string.feature_settings_impl_brand_guidelines)) } NiaTextButton( onClick = { uriHandler.openUri(FEEDBACK_URL) }, ) { - Text(text = stringResource(string.feature_settings_feedback)) + Text(text = stringResource(string.feature_settings_impl_feedback)) } } } diff --git a/feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt b/feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModel.kt similarity index 92% rename from feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt rename to feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModel.kt index 123c84d1c..274f916d1 100644 --- a/feature/settings/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt +++ b/feature/settings/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModel.kt @@ -14,15 +14,15 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.settings +package com.google.samples.apps.nowinandroid.feature.settings.impl import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Loading +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Success import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.StateFlow diff --git a/feature/settings/impl/src/main/res/values/strings.xml b/feature/settings/impl/src/main/res/values/strings.xml new file mode 100644 index 000000000..18e0dcf18 --- /dev/null +++ b/feature/settings/impl/src/main/res/values/strings.xml @@ -0,0 +1,37 @@ + + + + Settings + Search + Settings + Loading… + Privacy policy + Licenses + Brand Guidelines + Feedback + Theme + Default + Android + Dark mode preference + System default + Light + Dark + Use Dynamic Color + Yes + No + OK + diff --git a/feature/settings/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModelTest.kt b/feature/settings/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModelTest.kt similarity index 90% rename from feature/settings/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModelTest.kt rename to feature/settings/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModelTest.kt index f977612a1..1d275f745 100644 --- a/feature/settings/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModelTest.kt +++ b/feature/settings/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/settings/impl/SettingsViewModelTest.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.settings +package com.google.samples.apps.nowinandroid.feature.settings.impl import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading -import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Loading +import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Success import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml deleted file mode 100644 index 887539bd5..000000000 --- a/feature/settings/src/main/res/values/strings.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - Settings - Search - Settings - Loading… - Privacy policy - Licenses - Brand Guidelines - Feedback - Theme - Default - Android - Dark mode preference - System default - Light - Dark - Use Dynamic Color - Yes - No - OK - diff --git a/feature/topic/README.md b/feature/topic/README.md deleted file mode 100644 index 84588929c..000000000 --- a/feature/topic/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# :feature:topic module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_feature_topic.svg) diff --git a/feature/topic/api/.gitignore b/feature/topic/api/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/topic/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/topic/api/README.md b/feature/topic/api/README.md new file mode 100644 index 000000000..62103d57b --- /dev/null +++ b/feature/topic/api/README.md @@ -0,0 +1,3 @@ +# :feature:topic:api module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_feature_topic_api.svg) diff --git a/feature/topic/api/build.gradle.kts b/feature/topic/api/build.gradle.kts new file mode 100644 index 000000000..923a9b38b --- /dev/null +++ b/feature/topic/api/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.nowinandroid.android.feature.api) + alias(libs.plugins.nowinandroid.android.feature.impl) + alias(libs.plugins.nowinandroid.android.library.compose) +} + +android { + namespace = "com.google.samples.apps.nowinandroid.feature.topic.api" +} \ No newline at end of file diff --git a/feature/topic/src/main/AndroidManifest.xml b/feature/topic/api/src/main/AndroidManifest.xml similarity index 100% rename from feature/topic/src/main/AndroidManifest.xml rename to feature/topic/api/src/main/AndroidManifest.xml diff --git a/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicNavKey.kt b/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicNavKey.kt new file mode 100644 index 000000000..eb10b0cdd --- /dev/null +++ b/feature/topic/api/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/api/navigation/TopicNavKey.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.topic.api.navigation + +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import kotlinx.serialization.Serializable + +@Serializable +data class TopicNavKey(val id: String) : NavKey + +fun Navigator.navigateToTopic( + topicId: String, +) { + navigate(TopicNavKey(topicId)) +} diff --git a/feature/topic/src/main/res/values/strings.xml b/feature/topic/api/src/main/res/values/strings.xml similarity index 82% rename from feature/topic/src/main/res/values/strings.xml rename to feature/topic/api/src/main/res/values/strings.xml index fe4a6dc29..1e3f376cf 100644 --- a/feature/topic/src/main/res/values/strings.xml +++ b/feature/topic/api/src/main/res/values/strings.xml @@ -15,6 +15,5 @@ limitations under the License. --> - Loading topic - Select an Interest + Loading topic diff --git a/feature/topic/impl/.gitignore b/feature/topic/impl/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/topic/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/topic/impl/README.md b/feature/topic/impl/README.md new file mode 100644 index 000000000..eee690ec0 --- /dev/null +++ b/feature/topic/impl/README.md @@ -0,0 +1,3 @@ +# :feature:topic:impl module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_feature_topic_impl.svg) diff --git a/feature/topic/build.gradle.kts b/feature/topic/impl/build.gradle.kts similarity index 85% rename from feature/topic/build.gradle.kts rename to feature/topic/impl/build.gradle.kts index bd8b59ec8..fdf37c32e 100644 --- a/feature/topic/build.gradle.kts +++ b/feature/topic/impl/build.gradle.kts @@ -15,17 +15,20 @@ */ plugins { - alias(libs.plugins.nowinandroid.android.feature) + alias(libs.plugins.nowinandroid.android.feature.impl) alias(libs.plugins.nowinandroid.android.library.compose) alias(libs.plugins.nowinandroid.android.library.jacoco) } android { - namespace = "com.google.samples.apps.nowinandroid.feature.topic" + namespace = "com.google.samples.apps.nowinandroid.feature.topic.impl" } dependencies { implementation(projects.core.data) + implementation(projects.feature.topic.api) + + implementation(libs.androidx.compose.material3.adaptive.navigation3) testImplementation(projects.core.testing) testImplementation(libs.robolectric) diff --git a/feature/topic/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt b/feature/topic/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreenTest.kt similarity index 95% rename from feature/topic/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt rename to feature/topic/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreenTest.kt index 2b87baf9e..5f6782160 100644 --- a/feature/topic/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt +++ b/feature/topic/impl/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreenTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.topic +package com.google.samples.apps.nowinandroid.feature.topic.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.hasScrollToNodeAction @@ -26,6 +26,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performScrollToNode import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData +import com.google.samples.apps.nowinandroid.feature.topic.api.R import org.junit.Before import org.junit.Rule import org.junit.Test @@ -45,7 +46,7 @@ class TopicScreenTest { @Before fun setup() { composeTestRule.activity.apply { - topicLoading = getString(R.string.feature_topic_loading) + topicLoading = getString(R.string.feature_topic_api_loading) } } diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreen.kt similarity index 96% rename from feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt rename to feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreen.kt index 8ef0d786d..5c802f225 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.topic +package com.google.samples.apps.nowinandroid.feature.topic.impl import androidx.annotation.VisibleForTesting import androidx.compose.foundation.gestures.Orientation @@ -50,7 +50,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground @@ -68,7 +68,8 @@ import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider import com.google.samples.apps.nowinandroid.core.ui.userNewsResourceCardItems -import com.google.samples.apps.nowinandroid.feature.topic.R.string +import com.google.samples.apps.nowinandroid.core.ui.R as UiR +import com.google.samples.apps.nowinandroid.feature.topic.api.R as TopicR @Composable fun TopicScreen( @@ -124,7 +125,7 @@ internal fun TopicScreen( TopicUiState.Loading -> item { NiaLoadingWheel( modifier = modifier, - contentDesc = stringResource(id = string.feature_topic_loading), + contentDesc = stringResource(id = TopicR.string.feature_topic_api_loading), ) } @@ -292,7 +293,7 @@ private fun TopicToolbar( Icon( imageVector = NiaIcons.ArrowBack, contentDescription = stringResource( - id = com.google.samples.apps.nowinandroid.core.ui.R.string.core_ui_back, + id = UiR.string.core_ui_back, ), ) } diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModel.kt similarity index 98% rename from feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt rename to feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModel.kt index 8865da463..8f780f025 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.topic +package com.google.samples.apps.nowinandroid.feature.topic.impl import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt new file mode 100644 index 000000000..b091ca701 --- /dev/null +++ b/feature/topic/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/navigation/TopicEntryProvider.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.feature.topic.impl.navigation + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.google.samples.apps.nowinandroid.core.navigation.Navigator +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.TopicNavKey +import com.google.samples.apps.nowinandroid.feature.topic.api.navigation.navigateToTopic +import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicScreen +import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicViewModel +import com.google.samples.apps.nowinandroid.feature.topic.impl.TopicViewModel.Factory + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +fun EntryProviderScope.topicEntry(navigator: Navigator) { + entry( + metadata = ListDetailSceneStrategy.detailPane(), + ) { key -> + val id = key.id + TopicScreen( + showBackButton = true, + onBackClick = { navigator.goBack() }, + onTopicClick = navigator::navigateToTopic, + viewModel = hiltViewModel( + key = id, + ) { factory -> + factory.create(id) + }, + ) + } +} diff --git a/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt b/feature/topic/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModelTest.kt similarity index 99% rename from feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt rename to feature/topic/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModelTest.kt index 58242110d..1bbf844c1 100644 --- a/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt +++ b/feature/topic/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/impl/TopicViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.topic +package com.google.samples.apps.nowinandroid.feature.topic.impl import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt deleted file mode 100644 index 69059c81d..000000000 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.samples.apps.nowinandroid.feature.topic.navigation - -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptionsBuilder -import androidx.navigation.compose.composable -import androidx.navigation.toRoute -import com.google.samples.apps.nowinandroid.feature.topic.TopicScreen -import com.google.samples.apps.nowinandroid.feature.topic.TopicViewModel -import kotlinx.serialization.Serializable - -@Serializable data class TopicRoute(val id: String) - -fun NavController.navigateToTopic(topicId: String, navOptions: NavOptionsBuilder.() -> Unit = {}) { - navigate(route = TopicRoute(topicId)) { - navOptions() - } -} - -fun NavGraphBuilder.topicScreen( - showBackButton: Boolean, - onBackClick: () -> Unit, - onTopicClick: (String) -> Unit, -) { - composable { entry -> - val id = entry.toRoute().id - TopicScreen( - showBackButton = showBackButton, - onBackClick = onBackClick, - onTopicClick = onTopicClick, - viewModel = hiltViewModel( - key = id, - ) { factory -> - factory.create(id) - }, - ) - } -} diff --git a/generateModuleGraphs.sh b/generateModuleGraphs.sh deleted file mode 100755 index 5307d2932..000000000 --- a/generateModuleGraphs.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/bin/bash -# -# 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. -# -# -# Script to generate dependency graphs for each of the modules. The --exclude-module parameter can -# be used to exclude modules which are not part of the root dependency graph (and which, if included -# would cause the script to fail. -# -# Usage: generateModuleGraphs.sh --exclude-module :benchmarks --exclude-module :lint --exclude-module :ui-test-hilt-manifest - -# Check if the dot command is available -if ! command -v dot &> /dev/null -then - echo "The 'dot' command is not found. This is required to generate SVGs from the Graphviz files." - echo "Installation instructions:" - echo " - On macOS: You can install Graphviz using Homebrew with the command: 'brew install graphviz'" - echo " - On Ubuntu: You can install Graphviz using APT with the command: 'sudo apt install graphviz'" - echo " - Others: Visit https://graphviz.org/download/" - exit 1 -fi - -# Check if the svgo command is available -if ! command -v svgo &> /dev/null -then - echo "The 'svgo' command is not found. This is required to cleanup and compress SVGs." - echo "Installation instructions available at https://github.com/svg/svgo." - exit 1 -fi - -# Check for a version of grep which supports Perl regex. -# On MacOS the OS installed grep doesn't support Perl regex so check for the existence of the -# GNU version instead which is prefixed with 'g' to distinguish it from the OS installed version. - if grep -P "" /dev/null > /dev/null 2>&1; then - GREP_COMMAND=grep -elif command -v ggrep &> /dev/null; then - GREP_COMMAND=ggrep -else - echo "You don't have a version of 'grep' installed which supports Perl regular expressions." - echo "On MacOS you can install one using Homebrew with the command: 'brew install grep'" - exit 1 -fi - -# Initialize an array to store excluded modules -excluded_modules=() - -# Parse command-line arguments for excluded modules -while [[ $# -gt 0 ]]; do - case "$1" in - --exclude-module) - excluded_modules+=("$2") - shift # Past argument - shift # Past value - ;; - *) - echo "Unknown parameter passed: $1" - exit 1 - ;; - esac -done - -# Get the module paths -module_paths=$(${GREP_COMMAND} -oP 'include\("\K[^"]+' settings.gradle.kts) - -# Ensure the output directory exists -mkdir -p docs/images/graphs/ - -# Function to check and create a README.md for modules which don't have one. -check_and_create_readme() { - local module_path="$1" - local file_name="$2" - - local readme_path="${module_path:1}" # Remove leading colon - readme_path=${readme_path//:/\/} # Replace colons with slashes - readme_path="${readme_path}/README.md" #Append the filename - - # Check if README.md exists and create it if not - if [[ ! -f "$readme_path" ]]; then - echo "Creating README.md for ${module_path}" - - # Determine the depth of the module based on the number of colons - local depth=$(awk -F: '{print NF-1}' <<< "${module_path}") - - # Construct the relative image path with the correct number of "../" - local relative_image_path="../" - for ((i=1; i<$depth; i++)); do - relative_image_path+="../" - done - relative_image_path+="docs/images/graphs/${file_name}.svg" - - echo "# ${module_path} module" > "$readme_path" - echo "## Dependency graph" >> "$readme_path" - echo "![Dependency graph](${relative_image_path})" >> "$readme_path" - fi -} - -# Loop through each module path -echo "$module_paths" | while read -r module_path; do - # Check if the module is in the excluded list - if [[ ! " ${excluded_modules[@]} " =~ " ${module_path} " ]]; then - # Derive the filename from the module path - file_name="dep_graph${module_path//:/_}" # Replace colons with underscores - file_name="${file_name//-/_}" # Replace dashes with underscores - - check_and_create_readme "$module_path" "$file_name" - - # Generate the .gv file in a temporary location - # +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + :lint[lint]:::unknown + +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/settings.gradle.kts b/settings.gradle.kts index 2b8c6e45c..73a1d9d6a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -59,18 +59,24 @@ include(":core:datastore-test") include(":core:designsystem") include(":core:domain") include(":core:model") +include(":core:navigation") include(":core:network") include(":core:notifications") include(":core:screenshot-testing") include(":core:testing") include(":core:ui") -include(":feature:foryou") -include(":feature:interests") -include(":feature:bookmarks") -include(":feature:topic") -include(":feature:search") -include(":feature:settings") +include(":feature:foryou:api") +include(":feature:foryou:impl") +include(":feature:interests:api") +include(":feature:interests:impl") +include(":feature:bookmarks:api") +include(":feature:bookmarks:impl") +include(":feature:topic:api") +include(":feature:topic:impl") +include(":feature:search:api") +include(":feature:search:impl") +include(":feature:settings:impl") include(":lint") include(":sync:work") include(":sync:sync-test") diff --git a/sync/sync-test/README.md b/sync/sync-test/README.md index 78876290f..601724ef7 100644 --- a/sync/sync-test/README.md +++ b/sync/sync-test/README.md @@ -1,3 +1,81 @@ -# :sync:sync-test module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_sync_sync_test.svg) +# `:sync:sync-test` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :sync + direction TB + :sync:sync-test[sync-test]:::android-library + :sync:work[work]:::android-library + end + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:model[model]:::jvm-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + end + + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :core:model + :sync:sync-test -.-> :core:data + :sync:sync-test -.-> :sync:work + :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/sync/work/README.md b/sync/work/README.md index 2fe66d616..fab74f33c 100644 --- a/sync/work/README.md +++ b/sync/work/README.md @@ -1,3 +1,78 @@ -# :sync:work module -## Dependency graph -![Dependency graph](../../docs/images/graphs/dep_graph_sync_work.svg) +# `:sync:work` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + subgraph :sync + direction TB + :sync:work[work]:::android-library + end + subgraph :core + direction TB + :core:analytics[analytics]:::android-library + :core:common[common]:::jvm-library + :core:data[data]:::android-library + :core:database[database]:::android-library + :core:datastore[datastore]:::android-library + :core:datastore-proto[datastore-proto]:::android-library + :core:model[model]:::jvm-library + :core:network[network]:::android-library + :core:notifications[notifications]:::android-library + end + + :core:data -.-> :core:analytics + :core:data --> :core:common + :core:data --> :core:database + :core:data --> :core:datastore + :core:data --> :core:network + :core:data -.-> :core:notifications + :core:database --> :core:model + :core:datastore -.-> :core:common + :core:datastore --> :core:datastore-proto + :core:datastore --> :core:model + :core:network --> :core:common + :core:network --> :core:model + :core:notifications -.-> :core:common + :core:notifications --> :core:model + :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/ui-test-hilt-manifest/README.md b/ui-test-hilt-manifest/README.md new file mode 100644 index 000000000..eb4e4b1f7 --- /dev/null +++ b/ui-test-hilt-manifest/README.md @@ -0,0 +1,45 @@ +# `:ui-test-hilt-manifest` + +## Module dependency graph + + +```mermaid +--- +config: + layout: elk + elk: + nodePlacementStrategy: SIMPLE +--- +graph TB + :ui-test-hilt-manifest[ui-test-hilt-manifest]:::android-library + +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; +``` + +
+