diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ed5fa237d..28ec373b7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,10 +1,19 @@ -Thanks for submitting a pull request. Please include the following information. +_Thanks for submitting a pull request. Please include the following information._ **What I have done and why** -Include a summary of what your pull request contains, and why you have made these changes. + +_Include a summary of what your pull request contains, and why you have made these changes._ Fixes # +**How I'm testing it** + +_Choose at least one:_ +- Unit tests +- UI tests +- Screenshot tests +- N/A _(provide justification)_ + **Do tests pass?** - [ ] Run local tests on `DemoDebug` variant: `./gradlew testDemoDebug` - [ ] Check formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply` diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 001140a87..db326c380 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -17,6 +17,7 @@ jobs: permissions: contents: write + pull-requests: write timeout-minutes: 60 @@ -100,12 +101,13 @@ jobs: commit_message: "🤖 Updates screenshots" # Run local tests after screenshot tests to avoid wrong UP-TO-DATE. TODO: Ignore screenshots. - - name: Run local tests + - name: Run local tests and create report if: always() run: ./gradlew testDemoDebug :lint:test # Replace task exclusions with `-Pandroidx.baselineprofile.skipgeneration` when # https://android-review.googlesource.com/c/platform/frameworks/support/+/2602790 landed in a # release build + - name: Build all build type and flavor permutations run: ./gradlew :app:assemble :benchmarks:assemble -x pixel6Api33ProdNonMinifiedReleaseAndroidTest @@ -119,11 +121,11 @@ jobs: name: APKs path: '**/build/outputs/apk/**/*.apk' - - name: Upload test results (XML) + - name: Upload JVM local results (XML) if: always() uses: actions/upload-artifact@v4 with: - name: test-results + name: local-test-results path: '**/build/test-results/test*UnitTest/**.xml' - name: Check lint @@ -180,10 +182,7 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v3 - - name: Build projects before running emulator - run: ./gradlew packageDemoDebug packageDemoDebugAndroidTest - - - name: Run instrumentation tests + - name: Build projects and run instrumentation tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} @@ -193,9 +192,41 @@ jobs: heap-size: 600M script: ./gradlew connectedDemoDebugAndroidTest --daemon + - name: Run local tests (including Roborazzi) for the combined coverage report (only API 30) + if: matrix.api-level == 30 + # There is no need to verify Roborazzi tests to generate coverage. + run: ./gradlew testDemoDebugUnitTest -Proborazzi.test.verify=false # Add Prod if we ever add JVM tests for prod + + # Add `createProdDebugUnitTestCoverageReport` if we ever add JVM tests for prod + - name: Generate coverage reports for Debug variants (only API 30) + if: matrix.api-level == 30 + run: ./gradlew createDemoDebugCombinedCoverageReport + - name: Upload test reports if: always() uses: actions/upload-artifact@v4 with: name: test-reports-${{ matrix.api-level }} path: '**/build/reports/androidTests' + + - name: Display local test coverage (only API 30) + if: matrix.api-level == 30 + id: jacoco + uses: madrapps/jacoco-report@v1.6.1 + with: + title: Combined test coverage report + min-coverage-overall: 40 + min-coverage-changed-files: 60 + paths: | + ${{ github.workspace }}/**/build/reports/jacoco/**/*Report.xml + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload local coverage reports (XML + HTML) (only API 30) + if: matrix.api-level == 30 + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + if-no-files-found: error + compression-level: 1 + overwrite: false + path: '**/build/reports/jacoco/' diff --git a/app-nia-catalog/README.md b/app-nia-catalog/README.md index edbbb5e46..cf6d05f4f 100644 --- a/app-nia-catalog/README.md +++ b/app-nia-catalog/README.md @@ -1,3 +1,3 @@ # :app-nia-catalog module - -![Dependency graph](../docs/images/graphs/dep_graph_app_nia_catalog.png) +## Dependency graph +![Dependency graph](../docs/images/graphs/dep_graph_app_nia_catalog.svg) diff --git a/app-nia-catalog/build.gradle.kts b/app-nia-catalog/build.gradle.kts index 0991ea0c5..94d55b81c 100644 --- a/app-nia-catalog/build.gradle.kts +++ b/app-nia-catalog/build.gradle.kts @@ -59,7 +59,7 @@ android { // To publish on the Play store a private signing key is required, but to allow anyone // who clones the code to sign and run the release variant, use the debug signing key. // TODO: Abstract the signing configuration to a separate file to avoid hardcoding this. - signingConfig = signingConfigs.getByName("debug") + signingConfig = signingConfigs.named("debug").get() } } } diff --git a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt index 5a619eb1b..8a4bff7ab 100644 --- a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt +++ b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt @@ -2,70 +2,71 @@ androidx.activity:activity-compose:1.8.0 androidx.activity:activity-ktx:1.8.0 androidx.activity:activity:1.8.0 androidx.annotation:annotation-experimental:1.4.0 -androidx.annotation:annotation-jvm:1.7.0 -androidx.annotation:annotation:1.7.0 +androidx.annotation:annotation-jvm:1.7.1 +androidx.annotation:annotation:1.7.1 androidx.appcompat:appcompat-resources:1.6.1 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 androidx.autofill:autofill:1.0.0 -androidx.browser:browser:1.6.0 +androidx.browser:browser:1.8.0 androidx.collection:collection-jvm:1.4.0 androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 -androidx.compose.animation:animation-android:1.6.1 -androidx.compose.animation:animation-core-android:1.6.1 -androidx.compose.animation:animation-core:1.6.1 -androidx.compose.animation:animation:1.6.1 -androidx.compose.foundation:foundation-android:1.6.1 -androidx.compose.foundation:foundation-layout-android:1.6.1 -androidx.compose.foundation:foundation-layout:1.6.1 -androidx.compose.foundation:foundation:1.6.1 -androidx.compose.material3:material3-android:1.2.0 -androidx.compose.material3:material3:1.2.0 -androidx.compose.material:material-icons-core-android:1.6.1 -androidx.compose.material:material-icons-core:1.6.1 -androidx.compose.material:material-icons-extended-android:1.6.1 -androidx.compose.material:material-icons-extended:1.6.1 -androidx.compose.material:material-ripple-android:1.6.1 -androidx.compose.material:material-ripple:1.6.1 -androidx.compose.runtime:runtime-android:1.6.1 -androidx.compose.runtime:runtime-saveable-android:1.6.1 -androidx.compose.runtime:runtime-saveable:1.6.1 -androidx.compose.runtime:runtime:1.6.1 -androidx.compose.ui:ui-android:1.6.1 -androidx.compose.ui:ui-geometry-android:1.6.1 -androidx.compose.ui:ui-geometry:1.6.1 -androidx.compose.ui:ui-graphics-android:1.6.1 -androidx.compose.ui:ui-graphics:1.6.1 -androidx.compose.ui:ui-text-android:1.6.1 -androidx.compose.ui:ui-text:1.6.1 -androidx.compose.ui:ui-tooling-preview-android:1.6.1 -androidx.compose.ui:ui-tooling-preview:1.6.1 -androidx.compose.ui:ui-unit-android:1.6.1 -androidx.compose.ui:ui-unit:1.6.1 -androidx.compose.ui:ui-util-android:1.6.1 -androidx.compose.ui:ui-util:1.6.1 -androidx.compose.ui:ui:1.6.1 -androidx.compose:compose-bom:2024.02.00 +androidx.compose.animation:animation-android:1.6.3 +androidx.compose.animation:animation-core-android:1.6.3 +androidx.compose.animation:animation-core:1.6.3 +androidx.compose.animation:animation:1.6.3 +androidx.compose.foundation:foundation-android:1.6.3 +androidx.compose.foundation:foundation-layout-android:1.6.3 +androidx.compose.foundation:foundation-layout:1.6.3 +androidx.compose.foundation:foundation:1.6.3 +androidx.compose.material3:material3-android:1.2.1 +androidx.compose.material3:material3:1.2.1 +androidx.compose.material:material-icons-core-android:1.6.3 +androidx.compose.material:material-icons-core:1.6.3 +androidx.compose.material:material-icons-extended-android:1.6.3 +androidx.compose.material:material-icons-extended:1.6.3 +androidx.compose.material:material-ripple-android:1.6.3 +androidx.compose.material:material-ripple:1.6.3 +androidx.compose.runtime:runtime-android:1.6.3 +androidx.compose.runtime:runtime-saveable-android:1.6.3 +androidx.compose.runtime:runtime-saveable:1.6.3 +androidx.compose.runtime:runtime:1.6.3 +androidx.compose.ui:ui-android:1.6.3 +androidx.compose.ui:ui-geometry-android:1.6.3 +androidx.compose.ui:ui-geometry:1.6.3 +androidx.compose.ui:ui-graphics-android:1.6.3 +androidx.compose.ui:ui-graphics:1.6.3 +androidx.compose.ui:ui-text-android:1.6.3 +androidx.compose.ui:ui-text:1.6.3 +androidx.compose.ui:ui-tooling-preview-android:1.6.3 +androidx.compose.ui:ui-tooling-preview:1.6.3 +androidx.compose.ui:ui-unit-android:1.6.3 +androidx.compose.ui:ui-unit:1.6.3 +androidx.compose.ui:ui-util-android:1.6.3 +androidx.compose.ui:ui-util:1.6.3 +androidx.compose.ui:ui:1.6.3 +androidx.compose:compose-bom:2024.02.02 androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.12.0 androidx.core:core:1.12.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.0.0 androidx.emoji2:emoji2:1.3.0 -androidx.exifinterface:exifinterface:1.3.6 +androidx.exifinterface:exifinterface:1.3.7 androidx.fragment:fragment:1.5.1 androidx.interpolator:interpolator:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.6.2 -androidx.lifecycle:lifecycle-common:2.6.2 -androidx.lifecycle:lifecycle-livedata-core:2.6.2 -androidx.lifecycle:lifecycle-livedata:2.6.2 -androidx.lifecycle:lifecycle-process:2.6.2 -androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 -androidx.lifecycle:lifecycle-runtime:2.6.2 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2 -androidx.lifecycle:lifecycle-viewmodel:2.6.2 +androidx.lifecycle:lifecycle-common-java8:2.7.0 +androidx.lifecycle:lifecycle-common:2.7.0 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0 +androidx.lifecycle:lifecycle-livedata-core:2.7.0 +androidx.lifecycle:lifecycle-livedata:2.7.0 +androidx.lifecycle:lifecycle-process:2.7.0 +androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 +androidx.lifecycle:lifecycle-runtime:2.7.0 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 +androidx.lifecycle:lifecycle-viewmodel:2.7.0 androidx.loader:loader:1.0.0 androidx.metrics:metrics-performance:1.0.0-alpha04 androidx.profileinstaller:profileinstaller:1.3.1 @@ -86,16 +87,16 @@ com.google.dagger:hilt-android:2.51 com.google.dagger:hilt-core:2.51 com.google.guava:listenablefuture:1.0 com.squareup.okhttp3:okhttp:4.12.0 -com.squareup.okio:okio-jvm:3.6.0 -com.squareup.okio:okio:3.6.0 -io.coil-kt:coil-base:2.5.0 -io.coil-kt:coil-compose-base:2.5.0 -io.coil-kt:coil-compose:2.5.0 -io.coil-kt:coil:2.5.0 +com.squareup.okio:okio-jvm:3.8.0 +com.squareup.okio:okio:3.8.0 +io.coil-kt:coil-base:2.6.0 +io.coil-kt:coil-compose-base:2.6.0 +io.coil-kt:coil-compose:2.6.0 +io.coil-kt:coil:2.6.0 javax.inject:javax.inject:1 org.jetbrains.kotlin:kotlin-stdlib-common:1.9.22 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 org.jetbrains.kotlin:kotlin-stdlib:1.9.22 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 diff --git a/app/README.md b/app/README.md index 9f151c245..a3fb4572a 100644 --- a/app/README.md +++ b/app/README.md @@ -1,3 +1,3 @@ # :app module - -![Dependency graph](../docs/images/graphs/dep_graph_app.png) +## Dependency graph +![Dependency graph](../docs/images/graphs/dep_graph_app.svg) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 44a2bbfb5..34b5d7b95 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,7 +21,6 @@ plugins { alias(libs.plugins.nowinandroid.android.application.flavors) alias(libs.plugins.nowinandroid.android.application.jacoco) alias(libs.plugins.nowinandroid.android.hilt) - id("jacoco") alias(libs.plugins.nowinandroid.android.application.firebase) id("com.google.android.gms.oss-licenses-plugin") alias(libs.plugins.baselineprofile) @@ -53,7 +52,7 @@ android { // To publish on the Play store a private signing key is required, but to allow anyone // who clones the code to sign and run the release variant, use the debug signing key. // TODO: Abstract the signing configuration to a separate file to avoid hardcoding this. - signingConfig = signingConfigs.getByName("debug") + signingConfig = signingConfigs.named("debug").get() // Ensure Baseline Profile is fresh for release builds. baselineProfile.automaticGenerationDuringBuild = true } @@ -89,17 +88,23 @@ dependencies { implementation(projects.sync.work) implementation(libs.androidx.activity.compose) + 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.windowSizeClass) + implementation(libs.androidx.compose.runtime.tracing) implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) - implementation(libs.androidx.tracing.ktx) + implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.lifecycle.runtimeCompose) - implementation(libs.androidx.compose.runtime.tracing) - implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.profileinstaller) + implementation(libs.androidx.tracing.ktx) implementation(libs.kotlinx.coroutines.guava) implementation(libs.coil.kt) + ksp(libs.hilt.compiler) + debugImplementation(libs.androidx.compose.ui.testManifest) debugImplementation(projects.uiTestHiltManifest) diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt index f354c6fee..85adaf6fc 100644 --- a/app/dependencies/prodReleaseRuntimeClasspath.txt +++ b/app/dependencies/prodReleaseRuntimeClasspath.txt @@ -2,55 +2,61 @@ androidx.activity:activity-compose:1.8.0 androidx.activity:activity-ktx:1.8.0 androidx.activity:activity:1.8.0 androidx.annotation:annotation-experimental:1.4.0 -androidx.annotation:annotation-jvm:1.7.0 -androidx.annotation:annotation:1.7.0 +androidx.annotation:annotation-jvm:1.7.1 +androidx.annotation:annotation:1.7.1 androidx.appcompat:appcompat-resources:1.6.1 androidx.appcompat:appcompat:1.6.1 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 androidx.autofill:autofill:1.0.0 -androidx.browser:browser:1.6.0 +androidx.browser:browser:1.8.0 androidx.collection:collection-jvm:1.4.0 androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 -androidx.compose.animation:animation-android:1.6.1 -androidx.compose.animation:animation-core-android:1.6.1 -androidx.compose.animation:animation-core:1.6.1 -androidx.compose.animation:animation:1.6.1 -androidx.compose.foundation:foundation-android:1.6.1 -androidx.compose.foundation:foundation-layout-android:1.6.1 -androidx.compose.foundation:foundation-layout:1.6.1 -androidx.compose.foundation:foundation:1.6.1 -androidx.compose.material3:material3-android:1.2.0 -androidx.compose.material3:material3-window-size-class-android:1.2.0 -androidx.compose.material3:material3-window-size-class:1.2.0 -androidx.compose.material3:material3:1.2.0 -androidx.compose.material:material-icons-core-android:1.6.1 -androidx.compose.material:material-icons-core:1.6.1 -androidx.compose.material:material-icons-extended-android:1.6.1 -androidx.compose.material:material-icons-extended:1.6.1 -androidx.compose.material:material-ripple-android:1.6.1 -androidx.compose.material:material-ripple:1.6.1 -androidx.compose.runtime:runtime-android:1.6.1 -androidx.compose.runtime:runtime-saveable-android:1.6.1 -androidx.compose.runtime:runtime-saveable:1.6.1 +androidx.compose.animation:animation-android:1.6.3 +androidx.compose.animation:animation-core-android:1.6.3 +androidx.compose.animation:animation-core:1.6.3 +androidx.compose.animation:animation:1.6.3 +androidx.compose.foundation:foundation-android:1.6.3 +androidx.compose.foundation:foundation-layout-android:1.6.3 +androidx.compose.foundation:foundation-layout:1.6.3 +androidx.compose.foundation:foundation:1.6.3 +androidx.compose.material3.adaptive:adaptive-android:1.0.0-alpha08 +androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0-alpha08 +androidx.compose.material3.adaptive:adaptive-layout:1.0.0-alpha08 +androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0-alpha08 +androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-alpha08 +androidx.compose.material3.adaptive:adaptive:1.0.0-alpha08 +androidx.compose.material3:material3-android:1.2.1 +androidx.compose.material3:material3-window-size-class-android:1.2.1 +androidx.compose.material3:material3-window-size-class:1.2.1 +androidx.compose.material3:material3:1.2.1 +androidx.compose.material:material-icons-core-android:1.6.3 +androidx.compose.material:material-icons-core:1.6.3 +androidx.compose.material:material-icons-extended-android:1.6.3 +androidx.compose.material:material-icons-extended:1.6.3 +androidx.compose.material:material-ripple-android:1.6.3 +androidx.compose.material:material-ripple:1.6.3 +androidx.compose.runtime:runtime-android:1.6.3 +androidx.compose.runtime:runtime-saveable-android:1.6.3 +androidx.compose.runtime:runtime-saveable:1.6.3 androidx.compose.runtime:runtime-tracing:1.0.0-beta01 -androidx.compose.runtime:runtime:1.6.1 -androidx.compose.ui:ui-android:1.6.1 -androidx.compose.ui:ui-geometry-android:1.6.1 -androidx.compose.ui:ui-geometry:1.6.1 -androidx.compose.ui:ui-graphics-android:1.6.1 -androidx.compose.ui:ui-graphics:1.6.1 -androidx.compose.ui:ui-text-android:1.6.1 -androidx.compose.ui:ui-text:1.6.1 -androidx.compose.ui:ui-tooling-preview-android:1.6.1 -androidx.compose.ui:ui-tooling-preview:1.6.1 -androidx.compose.ui:ui-unit-android:1.6.1 -androidx.compose.ui:ui-unit:1.6.1 -androidx.compose.ui:ui-util-android:1.6.1 -androidx.compose.ui:ui-util:1.6.1 -androidx.compose.ui:ui:1.6.1 -androidx.compose:compose-bom:2024.02.00 +androidx.compose.runtime:runtime:1.6.3 +androidx.compose.ui:ui-android:1.6.3 +androidx.compose.ui:ui-geometry-android:1.6.3 +androidx.compose.ui:ui-geometry:1.6.3 +androidx.compose.ui:ui-graphics-android:1.6.3 +androidx.compose.ui:ui-graphics:1.6.3 +androidx.compose.ui:ui-text-android:1.6.3 +androidx.compose.ui:ui-text:1.6.3 +androidx.compose.ui:ui-tooling-preview-android:1.6.3 +androidx.compose.ui:ui-tooling-preview:1.6.3 +androidx.compose.ui:ui-unit-android:1.6.3 +androidx.compose.ui:ui-unit:1.6.3 +androidx.compose.ui:ui-util-android:1.6.3 +androidx.compose.ui:ui-util:1.6.3 +androidx.compose.ui:ui:1.6.3 +androidx.compose:compose-bom:2024.02.02 androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.12.0 androidx.core:core-splashscreen:1.0.1 @@ -66,11 +72,11 @@ androidx.documentfile:documentfile:1.0.0 androidx.drawerlayout:drawerlayout:1.0.0 androidx.emoji2:emoji2-views-helper:1.3.0 androidx.emoji2:emoji2:1.3.0 -androidx.exifinterface:exifinterface:1.3.6 +androidx.exifinterface:exifinterface:1.3.7 androidx.fragment:fragment:1.5.1 androidx.hilt:hilt-common:1.1.0 -androidx.hilt:hilt-navigation-compose:1.0.0 -androidx.hilt:hilt-navigation:1.0.0 +androidx.hilt:hilt-navigation-compose:1.2.0 +androidx.hilt:hilt-navigation:1.2.0 androidx.hilt:hilt-work:1.1.0 androidx.interpolator:interpolator:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0 @@ -116,12 +122,15 @@ androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 -androidx.window:window:1.0.0 +androidx.window.extensions.core:core:1.0.0 +androidx.window:window-core-android:1.3.0-alpha02 +androidx.window:window-core:1.3.0-alpha02 +androidx.window:window:1.3.0-alpha02 androidx.work:work-runtime-ktx:2.9.0 androidx.work:work-runtime:2.9.0 com.caverock:androidsvg-aar:1.4 com.google.accompanist:accompanist-drawablepainter:0.32.0 -com.google.accompanist:accompanist-permissions:0.32.0 +com.google.accompanist:accompanist-permissions:0.34.0 com.google.android.datatransport:transport-api:3.0.0 com.google.android.datatransport:transport-backend-cct:3.1.9 com.google.android.datatransport:transport-runtime:3.1.9 @@ -178,33 +187,31 @@ com.google.protobuf:protobuf-kotlin-lite:3.25.2 com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0 com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 -com.squareup.okio:okio-jvm:3.6.0 -com.squareup.okio:okio:3.6.0 +com.squareup.okio:okio-jvm:3.8.0 +com.squareup.okio:okio:3.8.0 com.squareup.retrofit2:retrofit:2.9.0 -io.coil-kt:coil-base:2.5.0 -io.coil-kt:coil-compose-base:2.5.0 -io.coil-kt:coil-compose:2.5.0 -io.coil-kt:coil-svg:2.5.0 -io.coil-kt:coil:2.5.0 -io.github.aakira:napier-android:1.4.1 -io.github.aakira:napier:1.4.1 +io.coil-kt:coil-base:2.6.0 +io.coil-kt:coil-compose-base:2.6.0 +io.coil-kt:coil-compose:2.6.0 +io.coil-kt:coil-svg:2.6.0 +io.coil-kt:coil:2.6.0 javax.inject:javax.inject:1 org.checkerframework:checker-qual:3.12.0 org.jetbrains.kotlin:kotlin-stdlib-common:1.9.22 -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 org.jetbrains.kotlin:kotlin-stdlib:1.9.22 -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3 -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.7.3 -org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 +org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.8.0 +org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.0 org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.5.0 org.jetbrains.kotlinx:kotlinx-datetime:0.5.0 -org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0 -org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.0 -org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0 -org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.0 -org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0 +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3 +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.3 +org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.3 +org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3 org.jetbrains:annotations:23.0.0 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 13f11c3b9..c9cc64120 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 @@ -274,10 +274,10 @@ class NavigationTest { // Select the last topic val topic = runBlocking { - topicsRepository.getTopics().first().sortedBy(Topic::name).last().name + topicsRepository.getTopics().first().sortedBy(Topic::name).last() } - onNodeWithTag("interests:topics").performScrollToNode(hasText(topic)) - onNodeWithText(topic).performClick() + onNodeWithTag("interests:topics").performScrollToNode(hasText(topic.name)) + onNodeWithText(topic.name).performClick() // Switch tab onNodeWithText(forYou).performClick() @@ -285,8 +285,8 @@ class NavigationTest { // Come back to Interests onNodeWithText(interests).performClick() - // Verify we're not in the list of interests - onNodeWithTag("interests:topics").assertDoesNotExist() + // Verify the topic is still shown + onNodeWithTag("topic:${topic.id}").assertExists() } } } diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index 732e527bb..7c9dfcc7a 100644 --- a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -167,7 +167,7 @@ class NiaAppStateTest { } @Test - fun stateIsOfflineWhenNetworkMonitorIsOffline() = runTest(UnconfinedTestDispatcher()) { + fun niaAppState_whenNetworkMonitorIsOffline_StateIsOffline() = runTest(UnconfinedTestDispatcher()) { composeTestRule.setContent { state = NiaAppState( navController = NavHostController(LocalContext.current), @@ -214,7 +214,7 @@ class NiaAppStateTest { @Composable private fun rememberTestNavController(): TestNavHostController { val context = LocalContext.current - return remember { + return remember { TestNavHostController(context).apply { navigatorProvider.addNavigator(ComposeNavigator()) graph = createGraph(startDestination = "a") { 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 index 6167b0b59..39bc03de7 100644 --- 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 @@ -22,12 +22,11 @@ import androidx.navigation.compose.NavHost import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen import com.google.samples.apps.nowinandroid.feature.foryou.navigation.FOR_YOU_ROUTE import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen -import com.google.samples.apps.nowinandroid.feature.interests.navigation.interestsGraph +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 @@ -49,24 +48,16 @@ fun NiaNavHost( startDestination = startDestination, modifier = modifier, ) { - forYouScreen(onTopicClick = navController::navigateToTopic) + forYouScreen(onTopicClick = navController::navigateToInterests) bookmarksScreen( - onTopicClick = navController::navigateToTopic, + onTopicClick = navController::navigateToInterests, onShowSnackbar = onShowSnackbar, ) searchScreen( onBackClick = navController::popBackStack, onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) }, - onTopicClick = navController::navigateToTopic, - ) - interestsGraph( - onTopicClick = navController::navigateToTopic, - nestedGraphs = { - topicScreen( - onBackClick = navController::popBackStack, - onTopicClick = navController::navigateToTopic, - ) - }, + onTopicClick = navController::navigateToInterests, ) + interestsListDetailScreen() } } 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 aa9ac4941..4658e3609 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 @@ -17,7 +17,6 @@ package com.google.samples.apps.nowinandroid.ui import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides @@ -78,7 +77,6 @@ import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR @OptIn( ExperimentalMaterial3Api::class, - ExperimentalLayoutApi::class, ExperimentalComposeUiApi::class, ) @Composable 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 d423adfbf..b653d8910 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 @@ -39,7 +39,7 @@ import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigat import com.google.samples.apps.nowinandroid.feature.foryou.navigation.FOR_YOU_ROUTE import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou import com.google.samples.apps.nowinandroid.feature.interests.navigation.INTERESTS_ROUTE -import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterestsGraph +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 @@ -173,7 +173,7 @@ class NiaAppState( when (topLevelDestination) { FOR_YOU -> navController.navigateToForYou(topLevelNavOptions) BOOKMARKS -> navController.navigateToBookmarks(topLevelNavOptions) - INTERESTS -> navController.navigateToInterestsGraph(topLevelNavOptions) + INTERESTS -> navController.navigateToInterests(null, topLevelNavOptions) } } } 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 new file mode 100644 index 000000000..d618c2d47 --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt @@ -0,0 +1,35 @@ +/* + * 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 com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@HiltViewModel +class Interests2PaneViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + val selectedTopicId: StateFlow = savedStateHandle.getStateFlow(TOPIC_ID_ARG, null) + + fun onTopicClick(topicId: String?) { + savedStateHandle[TOPIC_ID_ARG] = 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 new file mode 100644 index 000000000..335f83371 --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt @@ -0,0 +1,134 @@ +/* + * 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.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute +import com.google.samples.apps.nowinandroid.feature.interests.navigation.INTERESTS_ROUTE +import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG +import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder +import com.google.samples.apps.nowinandroid.feature.topic.navigation.TOPIC_ROUTE +import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic +import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen + +private const val DETAIL_PANE_NAVHOST_ROUTE = "detail_pane_route" + +fun NavGraphBuilder.interestsListDetailScreen() { + composable( + route = INTERESTS_ROUTE, + arguments = listOf( + navArgument(TOPIC_ID_ARG) { + type = NavType.StringType + defaultValue = null + nullable = true + }, + ), + ) { + InterestsListDetailScreen() + } +} + +@Composable +internal fun InterestsListDetailScreen( + viewModel: Interests2PaneViewModel = hiltViewModel(), +) { + val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle() + InterestsListDetailScreen( + selectedTopicId = selectedTopicId, + onTopicClick = viewModel::onTopicClick, + ) +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +internal fun InterestsListDetailScreen( + selectedTopicId: String?, + onTopicClick: (String) -> Unit, +) { + val listDetailNavigator = rememberListDetailPaneScaffoldNavigator() + BackHandler(listDetailNavigator.canNavigateBack()) { + listDetailNavigator.navigateBack() + } + + val nestedNavController = rememberNavController() + + fun onTopicClickShowDetailPane(topicId: String) { + onTopicClick(topicId) + nestedNavController.navigateToTopic(topicId) { + popUpTo(DETAIL_PANE_NAVHOST_ROUTE) + } + listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail) + } + + ListDetailPaneScaffold( + value = listDetailNavigator.scaffoldValue, + directive = listDetailNavigator.scaffoldDirective, + listPane = { + InterestsRoute( + onTopicClick = ::onTopicClickShowDetailPane, + highlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(), + ) + }, + detailPane = { + NavHost( + navController = nestedNavController, + startDestination = TOPIC_ROUTE, + route = DETAIL_PANE_NAVHOST_ROUTE, + ) { + topicScreen( + showBackButton = !listDetailNavigator.isListPaneVisible(), + onBackClick = listDetailNavigator::navigateBack, + onTopicClick = ::onTopicClickShowDetailPane, + ) + composable(route = TOPIC_ROUTE) { + TopicDetailPlaceholder() + } + } + }, + ) + LaunchedEffect(Unit) { + if (selectedTopicId != null) { + // Initial topic ID was provided when navigating to Interests, so show its details. + onTopicClickShowDetailPane(selectedTopicId) + } + } +} + +@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/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt index 4c3acc520..ac385b0d9 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt @@ -15,6 +15,7 @@ */ import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.gradle.internal.dsl.BaseAppModuleExtension import com.google.samples.apps.nowinandroid.configureJacoco import org.gradle.api.Plugin import org.gradle.api.Project @@ -23,13 +24,15 @@ import org.gradle.kotlin.dsl.getByType class AndroidApplicationJacocoConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - with(pluginManager) { - apply("org.gradle.jacoco") - apply("com.android.application") + pluginManager.apply("jacoco") + val androidExtension = extensions.getByType() + + androidExtension.buildTypes.configureEach { + enableAndroidTestCoverage = true + enableUnitTestCoverage = true } - val extension = extensions.getByType() - configureJacoco(extension) + + configureJacoco(extensions.getByType()) } } - -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt index 86ca091c3..6f2ff60c5 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +import com.android.build.api.dsl.LibraryExtension +import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.google.samples.apps.nowinandroid.configureJacoco import org.gradle.api.Plugin @@ -23,13 +25,15 @@ import org.gradle.kotlin.dsl.getByType class AndroidLibraryJacocoConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - with(pluginManager) { - apply("org.gradle.jacoco") - apply("com.android.library") + pluginManager.apply("jacoco") + val androidExtension = extensions.getByType() + + androidExtension.buildTypes.configureEach { + enableAndroidTestCoverage = true + enableUnitTestCoverage = true } - val extension = extensions.getByType() - configureJacoco(extension) + + configureJacoco(extensions.getByType()) } } - -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt index 29d31f9e6..dbca79a5e 100644 --- a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt @@ -15,6 +15,7 @@ */ import androidx.room.gradle.RoomExtension +import com.google.devtools.ksp.gradle.KspExtension import com.google.samples.apps.nowinandroid.libs import org.gradle.api.Plugin import org.gradle.api.Project @@ -28,6 +29,10 @@ class AndroidRoomConventionPlugin : Plugin { pluginManager.apply("androidx.room") pluginManager.apply("com.google.devtools.ksp") + extensions.configure { + arg("room.generateKotlin", "true") + } + extensions.configure { // The schemas directory contains a schema file for each version of the Room database. // This is required to enable Room auto migrations. 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 596c4f579..7820a978e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * 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. @@ -16,8 +16,13 @@ package com.google.samples.apps.nowinandroid +import com.android.build.api.artifact.ScopedArtifact import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.ScopedArtifacts import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.register @@ -32,13 +37,24 @@ private val coverageExclusions = listOf( "**/R.class", "**/R\$*.class", "**/BuildConfig.*", - "**/Manifest*.*" + "**/Manifest*.*", + "**/*_Hilt*.class", + "**/Hilt_*.class", ) private fun String.capitalize() = replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } +/** + * Creates a new task that generates a combined coverage report with data from local and + * instrumented tests. + * + * `create{variant}CombinedCoverageReport` + * + * Note that coverage data must exist before running the task. This allows us to run device + * tests on CI using a different Github Action or an external device farm. + */ internal fun Project.configureJacoco( androidComponentsExtension: AndroidComponentsExtension<*, *, *>, ) { @@ -46,37 +62,53 @@ internal fun Project.configureJacoco( toolVersion = libs.findVersion("jacoco").get().toString() } - val jacocoTestReport = tasks.create("jacocoTestReport") - androidComponentsExtension.onVariants { variant -> - val testTaskName = "test${variant.name.capitalize()}UnitTest" + val myObjFactory = project.objects val buildDir = layout.buildDirectory.get().asFile - val reportTask = tasks.register("jacoco${testTaskName.capitalize()}Report", JacocoReport::class) { - dependsOn(testTaskName) + val allJars: ListProperty = myObjFactory.listProperty(RegularFile::class.java) + val allDirectories: ListProperty = myObjFactory.listProperty(Directory::class.java) + val reportTask = + tasks.register("create${variant.name.capitalize()}CombinedCoverageReport", JacocoReport::class) { - reports { - xml.required.set(true) - html.required.set(true) - } - - classDirectories.setFrom( - fileTree("$buildDir/tmp/kotlin-classes/${variant.name}") { - exclude(coverageExclusions) + classDirectories.setFrom( + allJars, + allDirectories.map { dirs -> + dirs.map { dir -> + myObjFactory.fileTree().setDir(dir).exclude(coverageExclusions) + } + } + ) + reports { + xml.required.set(true) + html.required.set(true) } - ) - sourceDirectories.setFrom(files("$projectDir/src/main/java", "$projectDir/src/main/kotlin")) - executionData.setFrom(file("$buildDir/jacoco/$testTaskName.exec")) - } + // TODO: This is missing files in src/debug/, src/prod, src/demo, src/demoDebug... + sourceDirectories.setFrom(files("$projectDir/src/main/java", "$projectDir/src/main/kotlin")) + + executionData.setFrom( + project.fileTree("$buildDir/outputs/unit_test_code_coverage/${variant.name}UnitTest") + .matching { include("**/*.exec") }, + + project.fileTree("$buildDir/outputs/code_coverage/${variant.name}AndroidTest") + .matching { include("**/*.ec") } + ) + } + - jacocoTestReport.dependsOn(reportTask) + variant.artifacts.forScope(ScopedArtifacts.Scope.PROJECT) + .use(reportTask) + .toGet( + ScopedArtifact.CLASSES, + { _ -> allJars }, + { _ -> allDirectories }, + ) } tasks.withType().configureEach { configure { // Required for JaCoCo + Robolectric // https://github.com/robolectric/robolectric/issues/2230 - // TODO: Consider removing if not we don't add Robolectric isIncludeNoLocationClasses = true // Required for JDK 11 with the above diff --git a/build.gradle.kts b/build.gradle.kts index 6fe9b829e..1790cd202 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,9 +27,10 @@ buildscript { exclude(group = "com.google.protobuf") } } + } -// Lists all plugins used throughout the project without applying them. +// Lists all plugins used throughout the project plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false @@ -46,4 +47,15 @@ plugins { alias(libs.plugins.roborazzi) apply false alias(libs.plugins.secrets) apply false alias(libs.plugins.room) apply false + alias(libs.plugins.module.graph) apply true // Plugin applied to allow module graph generation } + +// Task to print all the module paths in the project e.g. :core:data +// Used by module graph generator script +tasks.register("printModulePaths") { + subprojects { + if (subprojects.size == 0) { + println(this.path) + } + } +} \ No newline at end of file diff --git a/core/analytics/README.md b/core/analytics/README.md new file mode 100644 index 000000000..d2bcd1ea7 --- /dev/null +++ b/core/analytics/README.md @@ -0,0 +1,3 @@ +# :core:analytics module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_analytics.svg) diff --git a/core/common/README.md b/core/common/README.md index ade22c076..96558bcc6 100644 --- a/core/common/README.md +++ b/core/common/README.md @@ -1,3 +1,3 @@ # :core:common module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_common.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_common.svg) diff --git a/core/data-test/README.md b/core/data-test/README.md index 4f623e629..977ee10e4 100644 --- a/core/data-test/README.md +++ b/core/data-test/README.md @@ -1,3 +1,3 @@ # :core:data-test module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_data_test.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_data_test.svg) diff --git a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt index 5ceff4dd0..070c7ed38 100644 --- a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt +++ b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt @@ -25,7 +25,7 @@ import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO -import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource +import com.google.samples.apps.nowinandroid.core.network.demo.DemoNiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -41,7 +41,7 @@ import javax.inject.Inject */ internal class FakeNewsRepository @Inject constructor( @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, - private val datasource: FakeNiaNetworkDataSource, + private val datasource: DemoNiaNetworkDataSource, ) : NewsRepository { override fun getNewsResources( diff --git a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeTopicsRepository.kt b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeTopicsRepository.kt index f8ebca29e..0b81dd309 100644 --- a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeTopicsRepository.kt +++ b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeTopicsRepository.kt @@ -21,7 +21,7 @@ import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepositor import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO -import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource +import com.google.samples.apps.nowinandroid.core.network.demo.DemoNiaNetworkDataSource import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -38,7 +38,7 @@ import javax.inject.Inject */ internal class FakeTopicsRepository @Inject constructor( @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, - private val datasource: FakeNiaNetworkDataSource, + private val datasource: DemoNiaNetworkDataSource, ) : TopicsRepository { override fun getTopics(): Flow> = flow { emit( diff --git a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeUserDataRepository.kt b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeUserDataRepository.kt index cdd23429f..4871baad9 100644 --- a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeUserDataRepository.kt +++ b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeUserDataRepository.kt @@ -43,7 +43,7 @@ internal class FakeUserDataRepository @Inject constructor( override suspend fun setTopicIdFollowed(followedTopicId: String, followed: Boolean) = niaPreferencesDataSource.setTopicIdFollowed(followedTopicId, followed) - override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) { + override suspend fun setNewsResourceBookmarked(newsResourceId: String, bookmarked: Boolean) { niaPreferencesDataSource.setNewsResourceBookmarked(newsResourceId, bookmarked) } diff --git a/core/data/README.md b/core/data/README.md index 905d74615..5d30f1638 100644 --- a/core/data/README.md +++ b/core/data/README.md @@ -1,3 +1,3 @@ # :core:data module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_data.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_data.svg) diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/SyncUtilities.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/SyncUtilities.kt index 5d069dbaf..878c8ee2f 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/SyncUtilities.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/SyncUtilities.kt @@ -19,8 +19,6 @@ package com.google.samples.apps.nowinandroid.core.data import android.util.Log import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlin.coroutines.cancellation.CancellationException /** @@ -104,29 +102,3 @@ suspend fun Synchronizer.changeListSync( versionUpdater(latestVersion) } }.isSuccess - -/** - * Returns a [Flow] whose values are generated by [transform] function that process the most - * recently emitted values by each flow. - */ -fun combine( - flow: Flow, - flow2: Flow, - flow3: Flow, - flow4: Flow, - flow5: Flow, - flow6: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6) -> R, -): Flow = combine( - combine(flow, flow2, flow3, ::Triple), - combine(flow4, flow5, flow6, ::Triple), -) { t1, t2 -> - transform( - t1.first, - t1.second, - t1.third, - t2.first, - t2.second, - t2.third, - ) -} diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt index c0b1bcc33..089b7087d 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt @@ -42,7 +42,7 @@ internal class OfflineFirstUserDataRepository @Inject constructor( analyticsHelper.logTopicFollowToggled(followedTopicId, followed) } - override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) { + override suspend fun setNewsResourceBookmarked(newsResourceId: String, bookmarked: Boolean) { niaPreferencesDataSource.setNewsResourceBookmarked(newsResourceId, bookmarked) analyticsHelper.logNewsResourceBookmarkToggled( newsResourceId = newsResourceId, diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt index ff616c179..c5202b02b 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt @@ -41,7 +41,7 @@ interface UserDataRepository { /** * Updates the bookmarked status for a news resource */ - suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) + suspend fun setNewsResourceBookmarked(newsResourceId: String, bookmarked: Boolean) /** * Updates the viewed status for a news resource diff --git a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt index 27e86f2f4..422e2cfb7 100644 --- a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt +++ b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt @@ -133,7 +133,7 @@ class OfflineFirstUserDataRepositoryTest { @Test fun offlineFirstUserDataRepository_bookmark_news_resource_logic_delegates_to_nia_preferences() = testScope.runTest { - subject.updateNewsResourceBookmark(newsResourceId = "0", bookmarked = true) + subject.setNewsResourceBookmarked(newsResourceId = "0", bookmarked = true) assertEquals( setOf("0"), @@ -142,7 +142,7 @@ class OfflineFirstUserDataRepositoryTest { .first(), ) - subject.updateNewsResourceBookmark(newsResourceId = "1", bookmarked = true) + subject.setNewsResourceBookmarked(newsResourceId = "1", bookmarked = true) assertEquals( setOf("0", "1"), diff --git a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNiaNetworkDataSource.kt b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNiaNetworkDataSource.kt index 7f9a69959..7675af7e9 100644 --- a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNiaNetworkDataSource.kt +++ b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNiaNetworkDataSource.kt @@ -17,7 +17,7 @@ package com.google.samples.apps.nowinandroid.core.data.testdoubles import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource -import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource +import com.google.samples.apps.nowinandroid.core.network.demo.DemoNiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic @@ -35,7 +35,7 @@ enum class CollectionType { */ class TestNiaNetworkDataSource : NiaNetworkDataSource { - private val source = FakeNiaNetworkDataSource( + private val source = DemoNiaNetworkDataSource( UnconfinedTestDispatcher(), Json { ignoreUnknownKeys = true }, ) diff --git a/core/database/README.md b/core/database/README.md index 5cf339aed..855eab53e 100644 --- a/core/database/README.md +++ b/core/database/README.md @@ -1,3 +1,3 @@ # :core:database module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_database.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_database.svg) diff --git a/core/database/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDaoTest.kt b/core/database/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDaoTest.kt index 55e817618..535ab61a7 100644 --- a/core/database/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDaoTest.kt +++ b/core/database/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceDaoTest.kt @@ -27,6 +27,7 @@ import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant +import org.junit.After import org.junit.Before import org.junit.Test import kotlin.test.assertEquals @@ -48,6 +49,9 @@ class NewsResourceDaoTest { topicDao = db.topicDao() } + @After + fun closeDb() = db.close() + @Test fun newsResourceDao_fetches_items_by_descending_publish_date() = runTest { val newsResourceEntities = listOf( diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DaosModule.kt b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DaosModule.kt similarity index 90% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DaosModule.kt rename to core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DaosModule.kt index afbbf42e6..e7456054e 100644 --- a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DaosModule.kt +++ b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DaosModule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * 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. @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.database +package com.google.samples.apps.nowinandroid.core.database.di +import com.google.samples.apps.nowinandroid.core.database.NiaDatabase import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceDao import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao import com.google.samples.apps.nowinandroid.core.database.dao.RecentSearchQueryDao diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DatabaseModule.kt similarity index 86% rename from core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt rename to core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DatabaseModule.kt index c6e33f284..d79d35948 100644 --- a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/DatabaseModule.kt +++ b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DatabaseModule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * 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. @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.database +package com.google.samples.apps.nowinandroid.core.database.di import android.content.Context import androidx.room.Room +import com.google.samples.apps.nowinandroid.core.database.NiaDatabase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/core/datastore-proto/README.md b/core/datastore-proto/README.md new file mode 100644 index 000000000..19ed58239 --- /dev/null +++ b/core/datastore-proto/README.md @@ -0,0 +1,3 @@ +# :core:datastore-proto module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_datastore_proto.svg) diff --git a/core/datastore-test/README.md b/core/datastore-test/README.md index 45cc51a48..99cf13f1f 100644 --- a/core/datastore-test/README.md +++ b/core/datastore-test/README.md @@ -1,3 +1,3 @@ # :core:datastore-test module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_datastore_test.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_datastore_test.svg) diff --git a/core/datastore/README.md b/core/datastore/README.md index 56699a483..4785c5885 100644 --- a/core/datastore/README.md +++ b/core/datastore/README.md @@ -1,3 +1,3 @@ # :core:datastore module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_datastore.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_datastore.svg) diff --git a/core/designsystem/README.md b/core/designsystem/README.md index 52a793821..d1778cb14 100644 --- a/core/designsystem/README.md +++ b/core/designsystem/README.md @@ -1,3 +1,3 @@ # :core:designsystem module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_designsystem.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_designsystem.svg) diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Chip.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Chip.kt index 9497bd92d..fe34cfaed 100644 --- a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Chip.kt +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Chip.kt @@ -18,7 +18,6 @@ package com.google.samples.apps.nowinandroid.core.designsystem.component import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon @@ -43,7 +42,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme * @param label The text label content. */ @Composable -@OptIn(ExperimentalMaterial3Api::class) fun NiaFilterChip( selected: Boolean, onSelectedChange: (Boolean) -> Unit, diff --git a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/FilterChipScreenshotTests.kt b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/FilterChipScreenshotTests.kt index cd2ab2082..7a6a92a1d 100644 --- a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/FilterChipScreenshotTests.kt +++ b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/FilterChipScreenshotTests.kt @@ -48,7 +48,7 @@ import org.robolectric.annotation.LooperMode @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(application = HiltTestApplication::class, qualifiers = "480dpi") @LooperMode(LooperMode.Mode.PAUSED) -class FilterChipScreenshotTests() { +class FilterChipScreenshotTests { @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/LoadingWheelScreenshotTests.kt b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/LoadingWheelScreenshotTests.kt index 9f80d04e5..9bdaca670 100644 --- a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/LoadingWheelScreenshotTests.kt +++ b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/LoadingWheelScreenshotTests.kt @@ -39,7 +39,7 @@ import org.robolectric.annotation.LooperMode @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(application = HiltTestApplication::class, qualifiers = "480dpi") @LooperMode(LooperMode.Mode.PAUSED) -class LoadingWheelScreenshotTests() { +class LoadingWheelScreenshotTests { @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/NavigationScreenshotTests.kt b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/NavigationScreenshotTests.kt index 9545732ba..be2c6fa28 100644 --- a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/NavigationScreenshotTests.kt +++ b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/NavigationScreenshotTests.kt @@ -47,7 +47,7 @@ import org.robolectric.annotation.LooperMode @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(application = HiltTestApplication::class, qualifiers = "480dpi") @LooperMode(LooperMode.Mode.PAUSED) -class NavigationScreenshotTests() { +class NavigationScreenshotTests { @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TabsScreenshotTests.kt b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TabsScreenshotTests.kt index a5df8b530..8ab711505 100644 --- a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TabsScreenshotTests.kt +++ b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TabsScreenshotTests.kt @@ -45,7 +45,7 @@ import org.robolectric.annotation.LooperMode @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(application = HiltTestApplication::class, qualifiers = "480dpi") @LooperMode(LooperMode.Mode.PAUSED) -class TabsScreenshotTests() { +class TabsScreenshotTests { @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TagScreenshotTests.kt b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TagScreenshotTests.kt index eef7eb883..8a519942d 100644 --- a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TagScreenshotTests.kt +++ b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TagScreenshotTests.kt @@ -42,7 +42,7 @@ import org.robolectric.annotation.LooperMode @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(application = HiltTestApplication::class, qualifiers = "480dpi") @LooperMode(LooperMode.Mode.PAUSED) -class TagScreenshotTests() { +class TagScreenshotTests { @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TopAppBarScreenshotTests.kt b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TopAppBarScreenshotTests.kt index 8e4200e77..5988ed592 100644 --- a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TopAppBarScreenshotTests.kt +++ b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/TopAppBarScreenshotTests.kt @@ -45,7 +45,7 @@ import org.robolectric.annotation.LooperMode @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(application = HiltTestApplication::class, qualifiers = "480dpi") @LooperMode(LooperMode.Mode.PAUSED) -class TopAppBarScreenshotTests() { +class TopAppBarScreenshotTests { @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/core/domain/README.md b/core/domain/README.md new file mode 100644 index 000000000..cc6905846 --- /dev/null +++ b/core/domain/README.md @@ -0,0 +1,3 @@ +# :core:domain module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_domain.svg) diff --git a/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetSearchContentsCountUseCase.kt b/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetSearchContentsCountUseCase.kt deleted file mode 100644 index 3e3e1952e..000000000 --- a/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetSearchContentsCountUseCase.kt +++ /dev/null @@ -1,31 +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.core.domain - -import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository -import kotlinx.coroutines.flow.Flow -import javax.inject.Inject - -/** - * A use case which returns total count of *Fts tables - */ -class GetSearchContentsCountUseCase @Inject constructor( - private val searchContentsRepository: SearchContentsRepository, -) { - operator fun invoke(): Flow = - searchContentsRepository.getSearchContentsCount() -} diff --git a/core/model/README.md b/core/model/README.md index 5279064f2..efd0eec76 100644 --- a/core/model/README.md +++ b/core/model/README.md @@ -1,3 +1,3 @@ # :core:model module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_model.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_model.svg) diff --git a/core/network/README.md b/core/network/README.md index cfd9fd369..516aa2d38 100644 --- a/core/network/README.md +++ b/core/network/README.md @@ -1,3 +1,3 @@ # :core:network module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_network.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_network.svg) diff --git a/core/network/lint.xml b/core/network/lint.xml index 59fd50bd5..07c0cae9e 100644 --- a/core/network/lint.xml +++ b/core/network/lint.xml @@ -20,6 +20,6 @@ java.lang.IllegalStateException: () -> kotlin.String at org.jetbrains.kotlin.asJava.classes.KtLightClassForFacadeImpl$Companion.createForFacadeNoCache --> - + diff --git a/core/network/src/demo/kotlin/com/google/samples/apps/nowinandroid/core/network/di/FlavoredNetworkModule.kt b/core/network/src/demo/kotlin/com/google/samples/apps/nowinandroid/core/network/di/FlavoredNetworkModule.kt index 850b00201..42c2ffe8f 100644 --- a/core/network/src/demo/kotlin/com/google/samples/apps/nowinandroid/core/network/di/FlavoredNetworkModule.kt +++ b/core/network/src/demo/kotlin/com/google/samples/apps/nowinandroid/core/network/di/FlavoredNetworkModule.kt @@ -17,7 +17,7 @@ package com.google.samples.apps.nowinandroid.core.network.di import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource -import com.google.samples.apps.nowinandroid.core.network.fake.FakeNiaNetworkDataSource +import com.google.samples.apps.nowinandroid.core.network.demo.DemoNiaNetworkDataSource import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -28,5 +28,5 @@ import dagger.hilt.components.SingletonComponent internal interface FlavoredNetworkModule { @Binds - fun binds(impl: FakeNiaNetworkDataSource): NiaNetworkDataSource + fun binds(impl: DemoNiaNetworkDataSource): NiaNetworkDataSource } diff --git a/core/network/src/main/kotlin/JvmUnitTestFakeAssetManager.kt b/core/network/src/main/kotlin/JvmUnitTestDemoAssetManager.kt similarity index 88% rename from core/network/src/main/kotlin/JvmUnitTestFakeAssetManager.kt rename to core/network/src/main/kotlin/JvmUnitTestDemoAssetManager.kt index 79370d5a8..2ef418517 100644 --- a/core/network/src/main/kotlin/JvmUnitTestFakeAssetManager.kt +++ b/core/network/src/main/kotlin/JvmUnitTestDemoAssetManager.kt @@ -14,8 +14,7 @@ * limitations under the License. */ -import androidx.annotation.VisibleForTesting -import com.google.samples.apps.nowinandroid.core.network.fake.FakeAssetManager +import com.google.samples.apps.nowinandroid.core.network.demo.DemoAssetManager import java.io.File import java.io.InputStream import java.util.Properties @@ -25,8 +24,8 @@ import java.util.Properties * It must remain on the root package for an easier [Class.getResource] with relative paths. * @see UnitTestOptions */ -@VisibleForTesting -internal object JvmUnitTestFakeAssetManager : FakeAssetManager { + +internal object JvmUnitTestDemoAssetManager : DemoAssetManager { private val config = requireNotNull(javaClass.getResource("com/android/tools/test_config.properties")) { """ diff --git a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeAssetManager.kt b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/demo/DemoAssetManager.kt similarity index 81% rename from core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeAssetManager.kt rename to core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/demo/DemoAssetManager.kt index 53ad7d48d..e5fb07bb3 100644 --- a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeAssetManager.kt +++ b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/demo/DemoAssetManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * 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. @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.network.fake +package com.google.samples.apps.nowinandroid.core.network.demo import java.io.InputStream -fun interface FakeAssetManager { +fun interface DemoAssetManager { fun open(fileName: String): InputStream } diff --git a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSource.kt b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/demo/DemoNiaNetworkDataSource.kt similarity index 91% rename from core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSource.kt rename to core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/demo/DemoNiaNetworkDataSource.kt index 6ef90ecff..b7c912c00 100644 --- a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSource.kt +++ b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/demo/DemoNiaNetworkDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.network.fake +package com.google.samples.apps.nowinandroid.core.network.demo -import JvmUnitTestFakeAssetManager +import JvmUnitTestDemoAssetManager import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource @@ -33,10 +33,10 @@ import javax.inject.Inject /** * [NiaNetworkDataSource] implementation that provides static news resources to aid development */ -class FakeNiaNetworkDataSource @Inject constructor( +class DemoNiaNetworkDataSource @Inject constructor( @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, private val networkJson: Json, - private val assets: FakeAssetManager = JvmUnitTestFakeAssetManager, + private val assets: DemoAssetManager = JvmUnitTestDemoAssetManager, ) : NiaNetworkDataSource { @OptIn(ExperimentalSerializationApi::class) diff --git a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt index a68683c7c..a97540f2b 100644 --- a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt @@ -22,7 +22,7 @@ import coil.ImageLoader import coil.decode.SvgDecoder import coil.util.DebugLogger import com.google.samples.apps.nowinandroid.core.network.BuildConfig -import com.google.samples.apps.nowinandroid.core.network.fake.FakeAssetManager +import com.google.samples.apps.nowinandroid.core.network.demo.DemoAssetManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -46,9 +46,9 @@ internal object NetworkModule { @Provides @Singleton - fun providesFakeAssetManager( + fun providesDemoAssetManager( @ApplicationContext context: Context, - ): FakeAssetManager = FakeAssetManager(context.assets::open) + ): DemoAssetManager = DemoAssetManager(context.assets::open) @Provides @Singleton diff --git a/core/network/src/test/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSourceTest.kt b/core/network/src/test/kotlin/com/google/samples/apps/nowinandroid/core/network/demo/DemoNiaNetworkDataSourceTest.kt similarity index 90% rename from core/network/src/test/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSourceTest.kt rename to core/network/src/test/kotlin/com/google/samples/apps/nowinandroid/core/network/demo/DemoNiaNetworkDataSourceTest.kt index a0c60fdcb..e60cfeb3e 100644 --- a/core/network/src/test/kotlin/com/google/samples/apps/nowinandroid/core/network/fake/FakeNiaNetworkDataSourceTest.kt +++ b/core/network/src/test/kotlin/com/google/samples/apps/nowinandroid/core/network/demo/DemoNiaNetworkDataSourceTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.core.network.fake +package com.google.samples.apps.nowinandroid.core.network.demo -import JvmUnitTestFakeAssetManager +import JvmUnitTestDemoAssetManager import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import kotlinx.coroutines.test.StandardTestDispatcher @@ -29,18 +29,18 @@ import org.junit.Before import org.junit.Test import kotlin.test.assertEquals -class FakeNiaNetworkDataSourceTest { +class DemoNiaNetworkDataSourceTest { - private lateinit var subject: FakeNiaNetworkDataSource + private lateinit var subject: DemoNiaNetworkDataSource private val testDispatcher = StandardTestDispatcher() @Before fun setUp() { - subject = FakeNiaNetworkDataSource( + subject = DemoNiaNetworkDataSource( ioDispatcher = testDispatcher, networkJson = Json { ignoreUnknownKeys = true }, - assets = JvmUnitTestFakeAssetManager, + assets = JvmUnitTestDemoAssetManager, ) } diff --git a/core/notifications/README.md b/core/notifications/README.md new file mode 100644 index 000000000..8f5607bdf --- /dev/null +++ b/core/notifications/README.md @@ -0,0 +1,3 @@ +# :core:notifications module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_notifications.svg) diff --git a/core/screenshot-testing/README.md b/core/screenshot-testing/README.md new file mode 100644 index 000000000..9bd4f1f9c --- /dev/null +++ b/core/screenshot-testing/README.md @@ -0,0 +1,3 @@ +# :core:screenshot-testing module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_screenshot_testing.svg) diff --git a/core/testing/README.md b/core/testing/README.md index 8eea64ac9..5a35d379b 100644 --- a/core/testing/README.md +++ b/core/testing/README.md @@ -1,3 +1,3 @@ # :core:testing module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_testing.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_testing.svg) diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt index 504e79217..be76112dc 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt @@ -61,7 +61,7 @@ class TestUserDataRepository : UserDataRepository { } } - override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) { + override suspend fun setNewsResourceBookmarked(newsResourceId: String, bookmarked: Boolean) { currentUserData.let { current -> val bookmarkedNews = if (bookmarked) { current.bookmarkedNewsResources + newsResourceId diff --git a/core/ui/README.md b/core/ui/README.md index 88c3561f1..38e514d01 100644 --- a/core/ui/README.md +++ b/core/ui/README.md @@ -1,3 +1,3 @@ # :core:ui module - -![Dependency graph](../../docs/images/graphs/dep_graph_core_ui.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_core_ui.svg) diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/InterestsItem.kt similarity index 83% rename from feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt rename to core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/InterestsItem.kt index b055a3a14..28cd8d938 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsItem.kt +++ b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/InterestsItem.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.feature.interests +package com.google.samples.apps.nowinandroid.core.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -37,7 +38,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicA import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme -import com.google.samples.apps.nowinandroid.feature.interests.R.string +import com.google.samples.apps.nowinandroid.core.ui.R.string @Composable fun InterestsItem( @@ -49,6 +50,7 @@ fun InterestsItem( modifier: Modifier = Modifier, iconModifier: Modifier = Modifier, description: String = "", + isSelected: Boolean = false, ) { ListItem( leadingContent = { @@ -68,7 +70,7 @@ fun InterestsItem( Icon( imageVector = NiaIcons.Add, contentDescription = stringResource( - id = string.feature_interests_card_follow_button_content_desc, + id = string.core_ui_interests_card_follow_button_content_desc, ), ) }, @@ -76,17 +78,23 @@ fun InterestsItem( Icon( imageVector = NiaIcons.Check, contentDescription = stringResource( - id = string.feature_interests_card_unfollow_button_content_desc, + id = string.core_ui_interests_card_unfollow_button_content_desc, ), ) }, ) }, colors = ListItemDefaults.colors( - containerColor = Color.Transparent, + containerColor = if (isSelected) { + MaterialTheme.colorScheme.surfaceVariant + } else { + Color.Transparent + }, ), modifier = modifier - .semantics(mergeDescendants = true) { /* no-op */ } + .semantics(mergeDescendants = true) { + selected = isSelected + } .clickable(enabled = true, onClick = onClick), ) } @@ -179,3 +187,21 @@ private fun InterestsCardWithEmptyDescriptionPreview() { } } } + +@Preview +@Composable +private fun InterestsCardSelectedPreview() { + NiaTheme { + Surface { + InterestsItem( + name = "Compose", + description = "", + following = true, + topicImageUrl = "", + onClick = { }, + onFollowButtonClick = { }, + isSelected = true, + ) + } + } +} diff --git a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index e3fd29e9a..e60c498eb 100644 --- a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -33,7 +33,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -78,7 +77,6 @@ import java.util.Locale * [NewsResource] card used on the following screens: For You, Saved */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun NewsResourceCardExpanded( userNewsResource: UserNewsResource, diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 65a855fc9..ab76748ef 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -26,4 +26,7 @@ %1$s is followed %1$s is not followed + + Follow interest + Unfollow interest diff --git a/docs/ArchitectureLearningJourney.md b/docs/ArchitectureLearningJourney.md index 925858111..d98dadf1e 100644 --- a/docs/ArchitectureLearningJourney.md +++ b/docs/ArchitectureLearningJourney.md @@ -25,6 +25,8 @@ The app architecture has three layers: a [data layer](https://developer.android. Diagram showing overall app architecture +> [!NOTE] +> The official Android architecture is different from other architectures, such as "Clean Architecture". Concepts from other architectures may not apply here, or be applied in different ways. [More discussion here](https://github.com/android/nowinandroid/discussions/1273). The architecture follows a reactive programming model with [unidirectional data flow](https://developer.android.com/jetpack/guide/ui-layer#udf). With the data layer at the bottom, the key concepts are: diff --git a/docs/images/graphs/dep_graph_app.png b/docs/images/graphs/dep_graph_app.png deleted file mode 100644 index dc1e0b7d7..000000000 Binary files a/docs/images/graphs/dep_graph_app.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_app.svg b/docs/images/graphs/dep_graph_app.svg new file mode 100644 index 000000000..57a592a8e --- /dev/null +++ b/docs/images/graphs/dep_graph_app.svg @@ -0,0 +1,463 @@ + + + + + + +G + + + +:app + +:app + + + +:feature:interests + +:feature:interests + + + +:app->:feature:interests + + + + + +:feature:foryou + +:feature:foryou + + + +:app->:feature:foryou + + + + + +:feature:bookmarks + +:feature:bookmarks + + + +:app->:feature:bookmarks + + + + + +:feature:topic + +:feature:topic + + + +:app->:feature:topic + + + + + +:feature:search + +:feature:search + + + +:app->:feature:search + + + + + +:feature:settings + +:feature:settings + + + +:app->:feature:settings + + + + + +:core:common + +:core:common + + + +:app->:core:common + + + + + +:core:ui + +:core:ui + + + +:app->:core:ui + + + + + +:core:designsystem + +:core:designsystem + + + +:app->:core:designsystem + + + + + +:core:data + +:core:data + + + +:app->:core:data + + + + + +:core:model + +:core:model + + + +:app->:core:model + + + + + +:core:analytics + +:core:analytics + + + +:app->:core:analytics + + + + + +:sync:work + +:sync:work + + + +:app->:sync:work + + + + + +:feature:interests->:core:ui + + + + + +:feature:interests->:core:designsystem + + + + + +:feature:interests->:core:data + + + + + +:core:domain + +:core:domain + + + +:feature:interests->:core:domain + + + + + +:feature:foryou->:core:ui + + + + + +:feature:foryou->:core:designsystem + + + + + +:feature:foryou->:core:data + + + + + +:feature:foryou->:core:domain + + + + + +:feature:bookmarks->:core:ui + + + + + +:feature:bookmarks->:core:designsystem + + + + + +:feature:bookmarks->:core:data + + + + + +:feature:topic->:core:ui + + + + + +:feature:topic->:core:designsystem + + + + + +:feature:topic->:core:data + + + + + +:feature:search->:core:ui + + + + + +:feature:search->:core:designsystem + + + + + +:feature:search->:core:data + + + + + +:feature:search->:core:domain + + + + + +:feature:settings->:core:ui + + + + + +:feature:settings->:core:designsystem + + + + + +:feature:settings->:core:data + + + + + +:core:ui->:core:designsystem + + + + + +:core:ui->:core:model + + + + + +:core:ui->:core:analytics + + + + + +:core:data->:core:common + + + + + +:core:data->:core:analytics + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:sync:work->:core:data + + + + + +:sync:work->:core:analytics + + + + + +:core:domain->:core:data + + + + + +:core:domain->:core:model + + + + + +:core:database->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore->:core:model + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:common + + + + + +:core:network->:core:model + + + + + +:core:notifications->:core:common + + + + + +:core:notifications->:core:model + + + + + diff --git a/docs/images/graphs/dep_graph_app_nia_catalog.png b/docs/images/graphs/dep_graph_app_nia_catalog.png deleted file mode 100644 index e2698f0b0..000000000 Binary files a/docs/images/graphs/dep_graph_app_nia_catalog.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_app_nia_catalog.svg b/docs/images/graphs/dep_graph_app_nia_catalog.svg new file mode 100644 index 000000000..b58415cef --- /dev/null +++ b/docs/images/graphs/dep_graph_app_nia_catalog.svg @@ -0,0 +1,73 @@ + + + + + + +G + + + +:app-nia-catalog + +:app-nia-catalog + + + +:core:designsystem + +:core:designsystem + + + +:app-nia-catalog->:core:designsystem + + + + + +:core:ui + +:core:ui + + + +:app-nia-catalog->:core:ui + + + + + +:core:ui->:core:designsystem + + + + + +:core:analytics + +:core:analytics + + + +:core:ui->:core:analytics + + + + + +:core:model + +:core:model + + + +:core:ui->:core:model + + + + + diff --git a/docs/images/graphs/dep_graph_benchmark.png b/docs/images/graphs/dep_graph_benchmark.png deleted file mode 100644 index a724c2fca..000000000 Binary files a/docs/images/graphs/dep_graph_benchmark.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_analytics.svg b/docs/images/graphs/dep_graph_core_analytics.svg new file mode 100644 index 000000000..ac21c0707 --- /dev/null +++ b/docs/images/graphs/dep_graph_core_analytics.svg @@ -0,0 +1,19 @@ + + + + + + +G + + + +:core:analytics + +:core:analytics + + + diff --git a/docs/images/graphs/dep_graph_core_common.png b/docs/images/graphs/dep_graph_core_common.png deleted file mode 100644 index 8e5628068..000000000 Binary files a/docs/images/graphs/dep_graph_core_common.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_common.svg b/docs/images/graphs/dep_graph_core_common.svg new file mode 100644 index 000000000..c91f33853 --- /dev/null +++ b/docs/images/graphs/dep_graph_core_common.svg @@ -0,0 +1,19 @@ + + + + + + +G + + + +:core:common + +:core:common + + + diff --git a/docs/images/graphs/dep_graph_core_data.png b/docs/images/graphs/dep_graph_core_data.png deleted file mode 100644 index fc30029b7..000000000 Binary files a/docs/images/graphs/dep_graph_core_data.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_data.svg b/docs/images/graphs/dep_graph_core_data.svg new file mode 100644 index 000000000..cacf03a1f --- /dev/null +++ b/docs/images/graphs/dep_graph_core_data.svg @@ -0,0 +1,151 @@ + + + + + + +G + + + +:core:data + +:core:data + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:analytics + +:core:analytics + + + +:core:data->:core:analytics + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:core:model + +:core:model + + + +:core:database->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore->:core:model + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:common + + + + + +:core:network->:core:model + + + + + +:core:notifications->:core:common + + + + + +:core:notifications->:core:model + + + + + diff --git a/docs/images/graphs/dep_graph_core_data_test.png b/docs/images/graphs/dep_graph_core_data_test.png deleted file mode 100644 index c3762b600..000000000 Binary files a/docs/images/graphs/dep_graph_core_data_test.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_data_test.svg b/docs/images/graphs/dep_graph_core_data_test.svg new file mode 100644 index 000000000..162c83f10 --- /dev/null +++ b/docs/images/graphs/dep_graph_core_data_test.svg @@ -0,0 +1,163 @@ + + + + + + +G + + + +:core:data-test + +:core:data-test + + + +:core:data + +:core:data + + + +:core:data-test->:core:data + + + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:analytics + +:core:analytics + + + +:core:data->:core:analytics + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:core:model + +:core:model + + + +:core:database->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore->:core:model + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:common + + + + + +:core:network->:core:model + + + + + +:core:notifications->:core:common + + + + + +:core:notifications->:core:model + + + + + diff --git a/docs/images/graphs/dep_graph_core_database.png b/docs/images/graphs/dep_graph_core_database.png deleted file mode 100644 index dc3e65756..000000000 Binary files a/docs/images/graphs/dep_graph_core_database.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_database.svg b/docs/images/graphs/dep_graph_core_database.svg new file mode 100644 index 000000000..9e907b96f --- /dev/null +++ b/docs/images/graphs/dep_graph_core_database.svg @@ -0,0 +1,31 @@ + + + + + + +G + + + +:core:database + +:core:database + + + +:core:model + +:core:model + + + +:core:database->:core:model + + + + + diff --git a/docs/images/graphs/dep_graph_core_datastore.png b/docs/images/graphs/dep_graph_core_datastore.png deleted file mode 100644 index 861c2498a..000000000 Binary files a/docs/images/graphs/dep_graph_core_datastore.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_datastore.svg b/docs/images/graphs/dep_graph_core_datastore.svg new file mode 100644 index 000000000..cfcf78db2 --- /dev/null +++ b/docs/images/graphs/dep_graph_core_datastore.svg @@ -0,0 +1,55 @@ + + + + + + +G + + + +:core:datastore + +:core:datastore + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:model + +:core:model + + + +:core:datastore->:core:model + + + + + +:core:common + +:core:common + + + +:core:datastore->:core:common + + + + + diff --git a/docs/images/graphs/dep_graph_core_datastore_proto.svg b/docs/images/graphs/dep_graph_core_datastore_proto.svg new file mode 100644 index 000000000..d572d0ea7 --- /dev/null +++ b/docs/images/graphs/dep_graph_core_datastore_proto.svg @@ -0,0 +1,19 @@ + + + + + + +G + + + +:core:datastore-proto + +:core:datastore-proto + + + diff --git a/docs/images/graphs/dep_graph_core_datastore_test.png b/docs/images/graphs/dep_graph_core_datastore_test.png deleted file mode 100644 index efe51c7de..000000000 Binary files a/docs/images/graphs/dep_graph_core_datastore_test.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_datastore_test.svg b/docs/images/graphs/dep_graph_core_datastore_test.svg new file mode 100644 index 000000000..ca8d3f84b --- /dev/null +++ b/docs/images/graphs/dep_graph_core_datastore_test.svg @@ -0,0 +1,73 @@ + + + + + + +G + + + +:core:datastore-test + +:core:datastore-test + + + +:core:common + +:core:common + + + +:core:datastore-test->:core:common + + + + + +:core:datastore + +:core:datastore + + + +:core:datastore-test->:core:datastore + + + + + +:core:datastore->:core:common + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:model + +:core:model + + + +:core:datastore->:core:model + + + + + diff --git a/docs/images/graphs/dep_graph_core_designsystem.png b/docs/images/graphs/dep_graph_core_designsystem.png deleted file mode 100644 index 1d6002d2a..000000000 Binary files a/docs/images/graphs/dep_graph_core_designsystem.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_designsystem.svg b/docs/images/graphs/dep_graph_core_designsystem.svg new file mode 100644 index 000000000..f46f075f0 --- /dev/null +++ b/docs/images/graphs/dep_graph_core_designsystem.svg @@ -0,0 +1,19 @@ + + + + + + +G + + + +:core:designsystem + +:core:designsystem + + + diff --git a/docs/images/graphs/dep_graph_core_domain.svg b/docs/images/graphs/dep_graph_core_domain.svg new file mode 100644 index 000000000..1c97b64e8 --- /dev/null +++ b/docs/images/graphs/dep_graph_core_domain.svg @@ -0,0 +1,169 @@ + + + + + + +G + + + +:core:domain + +:core:domain + + + +:core:data + +:core:data + + + +:core:domain->:core:data + + + + + +:core:model + +:core:model + + + +:core:domain->:core:model + + + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:analytics + +:core:analytics + + + +:core:data->:core:analytics + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:core:database->:core:model + + + + + +:core:datastore->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:model + + + + + +:core:network->:core:common + + + + + +:core:notifications->:core:model + + + + + +:core:notifications->:core:common + + + + + diff --git a/docs/images/graphs/dep_graph_core_model.png b/docs/images/graphs/dep_graph_core_model.png deleted file mode 100644 index 205583afa..000000000 Binary files a/docs/images/graphs/dep_graph_core_model.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_model.svg b/docs/images/graphs/dep_graph_core_model.svg new file mode 100644 index 000000000..290457d6c --- /dev/null +++ b/docs/images/graphs/dep_graph_core_model.svg @@ -0,0 +1,19 @@ + + + + + + +G + + + +:core:model + +:core:model + + + diff --git a/docs/images/graphs/dep_graph_core_navigation.png b/docs/images/graphs/dep_graph_core_navigation.png deleted file mode 100644 index 5ceab49b8..000000000 Binary files a/docs/images/graphs/dep_graph_core_navigation.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_network.png b/docs/images/graphs/dep_graph_core_network.png deleted file mode 100644 index 908715660..000000000 Binary files a/docs/images/graphs/dep_graph_core_network.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_network.svg b/docs/images/graphs/dep_graph_core_network.svg new file mode 100644 index 000000000..ea804bcff --- /dev/null +++ b/docs/images/graphs/dep_graph_core_network.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +:core:network + +:core:network + + + +:core:common + +:core:common + + + +:core:network->:core:common + + + + + +:core:model + +:core:model + + + +:core:network->:core:model + + + + + diff --git a/docs/images/graphs/dep_graph_core_notifications.svg b/docs/images/graphs/dep_graph_core_notifications.svg new file mode 100644 index 000000000..cf25ca32e --- /dev/null +++ b/docs/images/graphs/dep_graph_core_notifications.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +:core:notifications + +:core:notifications + + + +:core:model + +:core:model + + + +:core:notifications->:core:model + + + + + +:core:common + +:core:common + + + +:core:notifications->:core:common + + + + + diff --git a/docs/images/graphs/dep_graph_core_screenshot_testing.svg b/docs/images/graphs/dep_graph_core_screenshot_testing.svg new file mode 100644 index 000000000..e9fc6d48a --- /dev/null +++ b/docs/images/graphs/dep_graph_core_screenshot_testing.svg @@ -0,0 +1,43 @@ + + + + + + +G + + + +:core:screenshot-testing + +:core:screenshot-testing + + + +:core:common + +:core:common + + + +:core:screenshot-testing->:core:common + + + + + +:core:designsystem + +:core:designsystem + + + +:core:screenshot-testing->:core:designsystem + + + + + diff --git a/docs/images/graphs/dep_graph_core_testing.png b/docs/images/graphs/dep_graph_core_testing.png deleted file mode 100644 index 162830ae7..000000000 Binary files a/docs/images/graphs/dep_graph_core_testing.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_testing.svg b/docs/images/graphs/dep_graph_core_testing.svg new file mode 100644 index 000000000..29d367e4b --- /dev/null +++ b/docs/images/graphs/dep_graph_core_testing.svg @@ -0,0 +1,199 @@ + + + + + + +G + + + +:core:testing + +:core:testing + + + +:core:analytics + +:core:analytics + + + +:core:testing->:core:analytics + + + + + +:core:data + +:core:data + + + +:core:testing->:core:data + + + + + +:core:model + +:core:model + + + +:core:testing->:core:model + + + + + +:core:notifications + +:core:notifications + + + +:core:testing->:core:notifications + + + + + +:core:common + +:core:common + + + +:core:testing->:core:common + + + + + +:core:designsystem + +:core:designsystem + + + +:core:testing->:core:designsystem + + + + + +:core:data->:core:analytics + + + + + +:core:data->:core:notifications + + + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:notifications->:core:model + + + + + +:core:notifications->:core:common + + + + + +:core:database->:core:model + + + + + +:core:datastore->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:model + + + + + +:core:network->:core:common + + + + + diff --git a/docs/images/graphs/dep_graph_core_ui.png b/docs/images/graphs/dep_graph_core_ui.png deleted file mode 100644 index 31c9e6715..000000000 Binary files a/docs/images/graphs/dep_graph_core_ui.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_core_ui.svg b/docs/images/graphs/dep_graph_core_ui.svg new file mode 100644 index 000000000..2eba46866 --- /dev/null +++ b/docs/images/graphs/dep_graph_core_ui.svg @@ -0,0 +1,55 @@ + + + + + + +G + + + +:core:ui + +:core:ui + + + +:core:analytics + +:core:analytics + + + +:core:ui->:core:analytics + + + + + +:core:designsystem + +:core:designsystem + + + +:core:ui->:core:designsystem + + + + + +:core:model + +:core:model + + + +:core:ui->:core:model + + + + + diff --git a/docs/images/graphs/dep_graph_feature_author.png b/docs/images/graphs/dep_graph_feature_author.png deleted file mode 100644 index ddd1f03f6..000000000 Binary files a/docs/images/graphs/dep_graph_feature_author.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_feature_bookmarks.png b/docs/images/graphs/dep_graph_feature_bookmarks.png deleted file mode 100644 index f07fe891e..000000000 Binary files a/docs/images/graphs/dep_graph_feature_bookmarks.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_feature_bookmarks.svg b/docs/images/graphs/dep_graph_feature_bookmarks.svg new file mode 100644 index 000000000..cfbb86412 --- /dev/null +++ b/docs/images/graphs/dep_graph_feature_bookmarks.svg @@ -0,0 +1,205 @@ + + + + + + +G + + + +:feature:bookmarks + +:feature:bookmarks + + + +:core:ui + +:core:ui + + + +:feature:bookmarks->:core:ui + + + + + +:core:designsystem + +:core:designsystem + + + +:feature:bookmarks->:core:designsystem + + + + + +:core:data + +:core:data + + + +:feature:bookmarks->:core:data + + + + + +:core:ui->:core:designsystem + + + + + +:core:analytics + +:core:analytics + + + +:core:ui->:core:analytics + + + + + +:core:model + +:core:model + + + +:core:ui->:core:model + + + + + +:core:data->:core:analytics + + + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:core:database->:core:model + + + + + +:core:datastore->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:model + + + + + +:core:network->:core:common + + + + + +:core:notifications->:core:model + + + + + +:core:notifications->:core:common + + + + + diff --git a/docs/images/graphs/dep_graph_feature_foryou.png b/docs/images/graphs/dep_graph_feature_foryou.png deleted file mode 100644 index cf483f1b1..000000000 Binary files a/docs/images/graphs/dep_graph_feature_foryou.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_feature_foryou.svg b/docs/images/graphs/dep_graph_feature_foryou.svg new file mode 100644 index 000000000..e196bc7da --- /dev/null +++ b/docs/images/graphs/dep_graph_feature_foryou.svg @@ -0,0 +1,229 @@ + + + + + + +G + + + +:feature:foryou + +:feature:foryou + + + +:core:ui + +:core:ui + + + +:feature:foryou->:core:ui + + + + + +:core:designsystem + +:core:designsystem + + + +:feature:foryou->:core:designsystem + + + + + +:core:data + +:core:data + + + +:feature:foryou->:core:data + + + + + +:core:domain + +:core:domain + + + +:feature:foryou->:core:domain + + + + + +:core:ui->:core:designsystem + + + + + +:core:analytics + +:core:analytics + + + +:core:ui->:core:analytics + + + + + +:core:model + +:core:model + + + +:core:ui->:core:model + + + + + +:core:data->:core:analytics + + + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:core:domain->:core:data + + + + + +:core:domain->:core:model + + + + + +:core:database->:core:model + + + + + +:core:datastore->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:model + + + + + +:core:network->:core:common + + + + + +:core:notifications->:core:model + + + + + +:core:notifications->:core:common + + + + + diff --git a/docs/images/graphs/dep_graph_feature_interests.png b/docs/images/graphs/dep_graph_feature_interests.png deleted file mode 100644 index 09c74f995..000000000 Binary files a/docs/images/graphs/dep_graph_feature_interests.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_feature_interests.svg b/docs/images/graphs/dep_graph_feature_interests.svg new file mode 100644 index 000000000..3728cb4f6 --- /dev/null +++ b/docs/images/graphs/dep_graph_feature_interests.svg @@ -0,0 +1,229 @@ + + + + + + +G + + + +:feature:interests + +:feature:interests + + + +:core:ui + +:core:ui + + + +:feature:interests->:core:ui + + + + + +:core:designsystem + +:core:designsystem + + + +:feature:interests->:core:designsystem + + + + + +:core:data + +:core:data + + + +:feature:interests->:core:data + + + + + +:core:domain + +:core:domain + + + +:feature:interests->:core:domain + + + + + +:core:ui->:core:designsystem + + + + + +:core:analytics + +:core:analytics + + + +:core:ui->:core:analytics + + + + + +:core:model + +:core:model + + + +:core:ui->:core:model + + + + + +:core:data->:core:analytics + + + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:core:domain->:core:data + + + + + +:core:domain->:core:model + + + + + +:core:database->:core:model + + + + + +:core:datastore->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:model + + + + + +:core:network->:core:common + + + + + +:core:notifications->:core:model + + + + + +:core:notifications->:core:common + + + + + diff --git a/docs/images/graphs/dep_graph_feature_search.svg b/docs/images/graphs/dep_graph_feature_search.svg new file mode 100644 index 000000000..24c90cb0c --- /dev/null +++ b/docs/images/graphs/dep_graph_feature_search.svg @@ -0,0 +1,229 @@ + + + + + + +G + + + +:feature:search + +:feature:search + + + +:core:ui + +:core:ui + + + +:feature:search->:core:ui + + + + + +:core:designsystem + +:core:designsystem + + + +:feature:search->:core:designsystem + + + + + +:core:data + +:core:data + + + +:feature:search->:core:data + + + + + +:core:domain + +:core:domain + + + +:feature:search->:core:domain + + + + + +:core:ui->:core:designsystem + + + + + +:core:analytics + +:core:analytics + + + +:core:ui->:core:analytics + + + + + +:core:model + +:core:model + + + +:core:ui->:core:model + + + + + +:core:data->:core:analytics + + + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:core:domain->:core:data + + + + + +:core:domain->:core:model + + + + + +:core:database->:core:model + + + + + +:core:datastore->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:model + + + + + +:core:network->:core:common + + + + + +:core:notifications->:core:model + + + + + +:core:notifications->:core:common + + + + + diff --git a/docs/images/graphs/dep_graph_feature_settings.svg b/docs/images/graphs/dep_graph_feature_settings.svg new file mode 100644 index 000000000..93826715a --- /dev/null +++ b/docs/images/graphs/dep_graph_feature_settings.svg @@ -0,0 +1,205 @@ + + + + + + +G + + + +:feature:settings + +:feature:settings + + + +:core:ui + +:core:ui + + + +:feature:settings->:core:ui + + + + + +:core:designsystem + +:core:designsystem + + + +:feature:settings->:core:designsystem + + + + + +:core:data + +:core:data + + + +:feature:settings->:core:data + + + + + +:core:ui->:core:designsystem + + + + + +:core:analytics + +:core:analytics + + + +:core:ui->:core:analytics + + + + + +:core:model + +:core:model + + + +:core:ui->:core:model + + + + + +:core:data->:core:analytics + + + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:core:database->:core:model + + + + + +:core:datastore->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:model + + + + + +:core:network->:core:common + + + + + +:core:notifications->:core:model + + + + + +:core:notifications->:core:common + + + + + diff --git a/docs/images/graphs/dep_graph_feature_topic.png b/docs/images/graphs/dep_graph_feature_topic.png deleted file mode 100644 index 8385d1ed6..000000000 Binary files a/docs/images/graphs/dep_graph_feature_topic.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_feature_topic.svg b/docs/images/graphs/dep_graph_feature_topic.svg new file mode 100644 index 000000000..cbda3c225 --- /dev/null +++ b/docs/images/graphs/dep_graph_feature_topic.svg @@ -0,0 +1,205 @@ + + + + + + +G + + + +:feature:topic + +:feature:topic + + + +:core:ui + +:core:ui + + + +:feature:topic->:core:ui + + + + + +:core:designsystem + +:core:designsystem + + + +:feature:topic->:core:designsystem + + + + + +:core:data + +:core:data + + + +:feature:topic->:core:data + + + + + +:core:ui->:core:designsystem + + + + + +:core:analytics + +:core:analytics + + + +:core:ui->:core:analytics + + + + + +:core:model + +:core:model + + + +:core:ui->:core:model + + + + + +:core:data->:core:analytics + + + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:core:database->:core:model + + + + + +:core:datastore->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:model + + + + + +:core:network->:core:common + + + + + +:core:notifications->:core:model + + + + + +:core:notifications->:core:common + + + + + diff --git a/docs/images/graphs/dep_graph_lint.png b/docs/images/graphs/dep_graph_lint.png deleted file mode 100644 index 176d1de5d..000000000 Binary files a/docs/images/graphs/dep_graph_lint.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_sync.png b/docs/images/graphs/dep_graph_sync.png deleted file mode 100644 index 26b79b9bc..000000000 Binary files a/docs/images/graphs/dep_graph_sync.png and /dev/null differ diff --git a/docs/images/graphs/dep_graph_sync_sync_test.svg b/docs/images/graphs/dep_graph_sync_sync_test.svg new file mode 100644 index 000000000..1e0753393 --- /dev/null +++ b/docs/images/graphs/dep_graph_sync_sync_test.svg @@ -0,0 +1,187 @@ + + + + + + +G + + + +:sync:sync-test + +:sync:sync-test + + + +:core:data + +:core:data + + + +:sync:sync-test->:core:data + + + + + +:sync:work + +:sync:work + + + +:sync:sync-test->:sync:work + + + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:analytics + +:core:analytics + + + +:core:data->:core:analytics + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:sync:work->:core:data + + + + + +:sync:work->:core:analytics + + + + + +:core:model + +:core:model + + + +:core:database->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore->:core:model + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:common + + + + + +:core:network->:core:model + + + + + +:core:notifications->:core:common + + + + + +:core:notifications->:core:model + + + + + diff --git a/docs/images/graphs/dep_graph_sync_work.svg b/docs/images/graphs/dep_graph_sync_work.svg new file mode 100644 index 000000000..6901b5761 --- /dev/null +++ b/docs/images/graphs/dep_graph_sync_work.svg @@ -0,0 +1,169 @@ + + + + + + +G + + + +:sync:work + +:sync:work + + + +:core:analytics + +:core:analytics + + + +:sync:work->:core:analytics + + + + + +:core:data + +:core:data + + + +:sync:work->:core:data + + + + + +:core:data->:core:analytics + + + + + +:core:common + +:core:common + + + +:core:data->:core:common + + + + + +:core:database + +:core:database + + + +:core:data->:core:database + + + + + +:core:datastore + +:core:datastore + + + +:core:data->:core:datastore + + + + + +:core:network + +:core:network + + + +:core:data->:core:network + + + + + +:core:notifications + +:core:notifications + + + +:core:data->:core:notifications + + + + + +:core:model + +:core:model + + + +:core:database->:core:model + + + + + +:core:datastore->:core:common + + + + + +:core:datastore->:core:model + + + + + +:core:datastore-proto + +:core:datastore-proto + + + +:core:datastore->:core:datastore-proto + + + + + +:core:network->:core:common + + + + + +:core:network->:core:model + + + + + +:core:notifications->:core:common + + + + + +:core:notifications->:core:model + + + + + diff --git a/feature/bookmarks/README.md b/feature/bookmarks/README.md index e2b8c65ac..54cbf91d0 100644 --- a/feature/bookmarks/README.md +++ b/feature/bookmarks/README.md @@ -1,3 +1,3 @@ # :feature:bookmarks module - -![Dependency graph](../../docs/images/graphs/dep_graph_feature_bookmarks.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_feature_bookmarks.svg) diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt index 7b6cac76a..f93602485 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt @@ -58,7 +58,7 @@ class BookmarksViewModel @Inject constructor( viewModelScope.launch { shouldDisplayUndoBookmark = true lastRemovedBookmarkId = newsResourceId - userDataRepository.updateNewsResourceBookmark(newsResourceId, false) + userDataRepository.setNewsResourceBookmarked(newsResourceId, false) } } @@ -71,7 +71,7 @@ class BookmarksViewModel @Inject constructor( fun undoBookmarkRemoval() { viewModelScope.launch { lastRemovedBookmarkId?.let { - userDataRepository.updateNewsResourceBookmark(it, true) + userDataRepository.setNewsResourceBookmarked(it, true) } } clearUndoState() diff --git a/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt b/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt index 6469a684b..037e9db64 100644 --- a/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt +++ b/feature/bookmarks/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt @@ -67,7 +67,7 @@ class BookmarksViewModelTest { val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() } newsRepository.sendNewsResources(newsResourcesTestData) - userDataRepository.updateNewsResourceBookmark(newsResourcesTestData[0].id, true) + userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true) val item = viewModel.feedUiState.value assertIs(item) assertEquals(item.feed.size, 1) @@ -81,7 +81,7 @@ class BookmarksViewModelTest { // Set the news resources to be used by this test newsRepository.sendNewsResources(newsResourcesTestData) // Start with the resource saved - userDataRepository.updateNewsResourceBookmark(newsResourcesTestData[0].id, true) + userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true) // Use viewModel to remove saved resource viewModel.removeFromSavedResources(newsResourcesTestData[0].id) // Verify list of saved resources is now empty diff --git a/feature/foryou/README.md b/feature/foryou/README.md index 1ca599859..0f08cb827 100644 --- a/feature/foryou/README.md +++ b/feature/foryou/README.md @@ -1,3 +1,3 @@ # :feature:foryou module - -![Dependency graph](../../docs/images/graphs/dep_graph_feature_foryou.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_feature_foryou.svg) diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index 2a4b6f4ec..85035a77a 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -117,7 +117,7 @@ class ForYouViewModel @Inject constructor( fun updateNewsResourceSaved(newsResourceId: String, isChecked: Boolean) { viewModelScope.launch { - userDataRepository.updateNewsResourceBookmark(newsResourceId, isChecked) + userDataRepository.setNewsResourceBookmarked(newsResourceId, isChecked) } } diff --git a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt index b75573975..2fbdf0a79 100644 --- a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt @@ -36,6 +36,7 @@ import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncManager import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.feature.foryou.navigation.LINKED_NEWS_RESOURCE_ID import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle @@ -70,6 +71,7 @@ class ForYouViewModelTest { topicsRepository = topicsRepository, userDataRepository = userDataRepository, ) + private val savedStateHandle = SavedStateHandle() private lateinit var viewModel: ForYouViewModel @@ -504,6 +506,24 @@ class ForYouViewModelTest { collectJob.cancel() } + + @Test + fun whenUpdateNewsResourceSavedIsCalled_bookmarkStateIsUpdated() = runTest { + val newsResourceId = "123" + viewModel.updateNewsResourceSaved(newsResourceId, true) + + assertEquals( + expected = setOf(newsResourceId), + actual = userDataRepository.userData.first().bookmarkedNewsResources, + ) + + viewModel.updateNewsResourceSaved(newsResourceId, false) + + assertEquals( + expected = emptySet(), + actual = userDataRepository.userData.first().bookmarkedNewsResources, + ) + } } private val sampleTopics = listOf( diff --git a/feature/interests/README.md b/feature/interests/README.md index b7601ecbc..90a4fbc9c 100644 --- a/feature/interests/README.md +++ b/feature/interests/README.md @@ -1,3 +1,3 @@ # :feature:interests module - -![Dependency graph](../../docs/images/graphs/dep_graph_feature_interests.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_feature_interests.svg) diff --git a/feature/interests/build.gradle.kts b/feature/interests/build.gradle.kts index f0bca9729..ee6aaf122 100644 --- a/feature/interests/build.gradle.kts +++ b/feature/interests/build.gradle.kts @@ -30,4 +30,4 @@ dependencies { testImplementation(projects.core.testing) androidTestImplementation(projects.core.testing) -} \ No newline at end of file +} diff --git a/feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt b/feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt index 4f9cbcc04..a441f5a9d 100644 --- a/feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt +++ b/feature/interests/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsScreenTest.kt @@ -27,10 +27,11 @@ 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 com.google.samples.apps.nowinandroid.feature.interests.R 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 /** * UI test for checking the correct behaviour of the Interests screen; @@ -50,12 +51,12 @@ class InterestsScreenTest { @Before fun setup() { composeTestRule.activity.apply { - interestsLoading = getString(R.string.feature_interests_loading) - interestsEmptyHeader = getString(R.string.feature_interests_empty_header) + interestsLoading = getString(InterestsR.string.feature_interests_loading) + interestsEmptyHeader = getString(InterestsR.string.feature_interests_empty_header) interestsTopicCardFollowButton = - getString(R.string.feature_interests_card_follow_button_content_desc) + getString(CoreUiR.string.core_ui_interests_card_follow_button_content_desc) interestsTopicCardUnfollowButton = - getString(R.string.feature_interests_card_unfollow_button_content_desc) + getString(CoreUiR.string.core_ui_interests_card_unfollow_button_content_desc) } } @@ -74,7 +75,10 @@ class InterestsScreenTest { fun interestsWithTopics_whenTopicsFollowed_showFollowedAndUnfollowedTopicsWithInfo() { composeTestRule.setContent { InterestsScreen( - uiState = InterestsUiState.Interests(topics = followableTopicTestData), + uiState = InterestsUiState.Interests( + topics = followableTopicTestData, + selectedTopicId = null, + ), ) } diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt index 5944b8631..468550878 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt +++ b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsScreen.kt @@ -35,9 +35,10 @@ import com.google.samples.apps.nowinandroid.core.ui.FollowableTopicPreviewParame import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent @Composable -internal fun InterestsRoute( +fun InterestsRoute( onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, + highlightSelectedTopic: Boolean = false, viewModel: InterestsViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -45,7 +46,11 @@ internal fun InterestsRoute( InterestsScreen( uiState = uiState, followTopic = viewModel::followTopic, - onTopicClick = onTopicClick, + onTopicClick = { + viewModel.onTopicClick(it) + onTopicClick(it) + }, + highlightSelectedTopic = highlightSelectedTopic, modifier = modifier, ) } @@ -56,6 +61,7 @@ internal fun InterestsScreen( followTopic: (String, Boolean) -> Unit, onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, + highlightSelectedTopic: Boolean = false, ) { Column( modifier = modifier, @@ -67,13 +73,17 @@ internal fun InterestsScreen( modifier = modifier, contentDesc = stringResource(id = R.string.feature_interests_loading), ) + is InterestsUiState.Interests -> TopicsTabContent( topics = uiState.topics, onTopicClick = onTopicClick, onFollowButtonClick = followTopic, + selectedTopicId = uiState.selectedTopicId, + highlightSelectedTopic = highlightSelectedTopic, modifier = modifier, ) + is InterestsUiState.Empty -> InterestsEmptyScreen() } } @@ -95,6 +105,7 @@ fun InterestsScreenPopulated( NiaBackground { InterestsScreen( uiState = InterestsUiState.Interests( + selectedTopicId = null, topics = followableTopics, ), followTopic = { _, _ -> }, diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt index 6d905a6d5..b369ac5ab 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt +++ b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt @@ -16,46 +16,57 @@ package com.google.samples.apps.nowinandroid.feature.interests +import androidx.lifecycle.SavedStateHandle 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.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.TOPIC_ID_ARG import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class InterestsViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, val userDataRepository: UserDataRepository, getFollowableTopics: GetFollowableTopicsUseCase, ) : ViewModel() { - val uiState: StateFlow = - getFollowableTopics(sortBy = TopicSortField.NAME).map( - InterestsUiState::Interests, - ).stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = InterestsUiState.Loading, - ) + val selectedTopicId: StateFlow = savedStateHandle.getStateFlow(TOPIC_ID_ARG, null) + + val uiState: StateFlow = combine( + selectedTopicId, + getFollowableTopics(sortBy = TopicSortField.NAME), + InterestsUiState::Interests, + ).stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = InterestsUiState.Loading, + ) fun followTopic(followedTopicId: String, followed: Boolean) { viewModelScope.launch { userDataRepository.setTopicIdFollowed(followedTopicId, followed) } } + + fun onTopicClick(topicId: String?) { + savedStateHandle[TOPIC_ID_ARG] = topicId + } } sealed interface InterestsUiState { data object Loading : InterestsUiState data class Interests( + val selectedTopicId: String?, val topics: List, ) : InterestsUiState diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt index d865f5e1a..83058c12e 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt +++ b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/TabContent.kt @@ -39,6 +39,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollba import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.rememberDraggableScroller import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic +import com.google.samples.apps.nowinandroid.core.ui.InterestsItem @Composable fun TopicsTabContent( @@ -47,6 +48,8 @@ fun TopicsTabContent( onFollowButtonClick: (String, Boolean) -> Unit, modifier: Modifier = Modifier, withBottomSpacer: Boolean = true, + selectedTopicId: String? = null, + highlightSelectedTopic: Boolean = false, ) { Box( modifier = modifier @@ -63,6 +66,7 @@ fun TopicsTabContent( topics.forEach { followableTopic -> val topicId = followableTopic.topic.id item(key = topicId) { + val isSelected = highlightSelectedTopic && topicId == selectedTopicId InterestsItem( name = followableTopic.topic.name, following = followableTopic.isFollowed, @@ -70,6 +74,7 @@ fun TopicsTabContent( topicImageUrl = followableTopic.topic.imageUrl, onClick = { onTopicClick(topicId) }, onFollowButtonClick = { onFollowButtonClick(topicId, it) }, + isSelected = isSelected, ) } } diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt index 2ad7c560b..8a0f2d130 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt +++ b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt @@ -19,26 +19,37 @@ package com.google.samples.apps.nowinandroid.feature.interests.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions +import androidx.navigation.NavType import androidx.navigation.compose.composable -import androidx.navigation.navigation +import androidx.navigation.navArgument import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute -private const val INTERESTS_GRAPH_ROUTE_PATTERN = "interests_graph" -const val INTERESTS_ROUTE = "interests_route" +const val TOPIC_ID_ARG = "topicId" +const val INTERESTS_ROUTE_BASE = "interests_route" +const val INTERESTS_ROUTE = "$INTERESTS_ROUTE_BASE?$TOPIC_ID_ARG={$TOPIC_ID_ARG}" -fun NavController.navigateToInterestsGraph(navOptions: NavOptions) = navigate(INTERESTS_GRAPH_ROUTE_PATTERN, navOptions) +fun NavController.navigateToInterests(topicId: String? = null, navOptions: NavOptions? = null) { + val route = if (topicId != null) { + "${INTERESTS_ROUTE_BASE}?${TOPIC_ID_ARG}=$topicId" + } else { + INTERESTS_ROUTE_BASE + } + navigate(route, navOptions) +} -fun NavGraphBuilder.interestsGraph( +fun NavGraphBuilder.interestsScreen( onTopicClick: (String) -> Unit, - nestedGraphs: NavGraphBuilder.() -> Unit, ) { - navigation( - route = INTERESTS_GRAPH_ROUTE_PATTERN, - startDestination = INTERESTS_ROUTE, + composable( + route = INTERESTS_ROUTE, + arguments = listOf( + navArgument(TOPIC_ID_ARG) { + defaultValue = null + nullable = true + type = NavType.StringType + }, + ), ) { - composable(route = INTERESTS_ROUTE) { - InterestsRoute(onTopicClick) - } - nestedGraphs() + InterestsRoute(onTopicClick = onTopicClick) } } diff --git a/feature/interests/src/main/res/values/strings.xml b/feature/interests/src/main/res/values/strings.xml index 2dd1c18a9..8d5322859 100644 --- a/feature/interests/src/main/res/values/strings.xml +++ b/feature/interests/src/main/res/values/strings.xml @@ -18,6 +18,4 @@ Interests Loading data "No available data" - Follow interest - Unfollow interest diff --git a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt b/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt index c46cb7780..63d3c49b7 100644 --- a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt +++ b/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt @@ -16,6 +16,7 @@ package com.google.samples.apps.nowinandroid.interests +import androidx.lifecycle.SavedStateHandle import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.Topic @@ -24,6 +25,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.feature.interests.InterestsUiState import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel +import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -53,6 +55,7 @@ class InterestsViewModelTest { @Before fun setup() { viewModel = InterestsViewModel( + savedStateHandle = SavedStateHandle(mapOf(TOPIC_ID_ARG to testInputTopics[0].topic.id)), userDataRepository = userDataRepository, getFollowableTopics = getFollowableTopicsUseCase, ) @@ -93,7 +96,10 @@ class InterestsViewModelTest { ) assertEquals( - InterestsUiState.Interests(topics = testOutputTopics), + InterestsUiState.Interests( + topics = testOutputTopics, + selectedTopicId = testInputTopics[0].topic.id, + ), viewModel.uiState.value, ) @@ -123,7 +129,10 @@ class InterestsViewModelTest { ) assertEquals( - InterestsUiState.Interests(topics = testInputTopics), + InterestsUiState.Interests( + topics = testInputTopics, + selectedTopicId = testInputTopics[0].topic.id, + ), viewModel.uiState.value, ) diff --git a/feature/search/README.md b/feature/search/README.md new file mode 100644 index 000000000..e205970f0 --- /dev/null +++ b/feature/search/README.md @@ -0,0 +1,3 @@ +# :feature:search module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_feature_search.svg) diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts index 206f4c0f9..98052e9ab 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/build.gradle.kts @@ -27,9 +27,7 @@ android { dependencies { implementation(projects.core.data) implementation(projects.core.domain) - implementation(projects.feature.bookmarks) - implementation(projects.feature.foryou) - implementation(projects.feature.interests) + implementation(projects.core.ui) testImplementation(projects.core.testing) diff --git a/feature/search/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt b/feature/search/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt index 8a0532e1b..a9e2fa98f 100644 --- a/feature/search/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt +++ b/feature/search/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreenTest.kt @@ -35,10 +35,10 @@ import com.google.samples.apps.nowinandroid.core.model.data.UserData 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 org.junit.Before import org.junit.Rule import org.junit.Test -import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR /** * UI test for checking the correct behaviour of the Search screen. @@ -73,9 +73,9 @@ class SearchScreenTest { clearSearchContentDesc = getString(R.string.feature_search_clear_search_text_content_desc) clearRecentSearchesContentDesc = getString(R.string.feature_search_clear_recent_searches_content_desc) followButtonContentDesc = - getString(interestsR.string.feature_interests_card_follow_button_content_desc) + getString(string.core_ui_interests_card_follow_button_content_desc) unfollowButtonContentDesc = - getString(interestsR.string.feature_interests_card_unfollow_button_content_desc) + 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) + diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt index ca159c80b..86b1eb717 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt +++ b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt @@ -55,7 +55,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -88,14 +87,11 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews +import com.google.samples.apps.nowinandroid.core.ui.InterestsItem 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.bookmarks.BookmarksViewModel -import com.google.samples.apps.nowinandroid.feature.foryou.ForYouViewModel -import com.google.samples.apps.nowinandroid.feature.interests.InterestsItem -import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel import com.google.samples.apps.nowinandroid.feature.search.R as searchR @Composable @@ -104,10 +100,7 @@ internal fun SearchRoute( onInterestsClick: () -> Unit, onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, - bookmarksViewModel: BookmarksViewModel = hiltViewModel(), - interestsViewModel: InterestsViewModel = hiltViewModel(), searchViewModel: SearchViewModel = hiltViewModel(), - forYouViewModel: ForYouViewModel = hiltViewModel(), ) { val recentSearchQueriesUiState by searchViewModel.recentSearchQueriesUiState.collectAsStateWithLifecycle() val searchResultUiState by searchViewModel.searchResultUiState.collectAsStateWithLifecycle() @@ -120,9 +113,9 @@ internal fun SearchRoute( onSearchQueryChanged = searchViewModel::onSearchQueryChanged, onSearchTriggered = searchViewModel::onSearchTriggered, onClearRecentSearches = searchViewModel::clearRecentSearches, - onNewsResourcesCheckedChanged = forYouViewModel::updateNewsResourceSaved, - onNewsResourceViewed = { bookmarksViewModel.setNewsResourceViewed(it, true) }, - onFollowButtonClick = interestsViewModel::followTopic, + onNewsResourcesCheckedChanged = searchViewModel::setNewsResourceBookmarked, + onNewsResourceViewed = { searchViewModel.setNewsResourceViewed(it, true) }, + onFollowButtonClick = searchViewModel::followTopic, onBackClick = onBackClick, onInterestsClick = onInterestsClick, onTopicClick = onTopicClick, @@ -470,7 +463,6 @@ private fun SearchToolbar( } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun SearchTextField( searchQuery: String, diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModel.kt b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModel.kt index 6dd93ceb6..6c2af240c 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModel.kt +++ b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModel.kt @@ -23,14 +23,15 @@ import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent.Param import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper import com.google.samples.apps.nowinandroid.core.data.repository.RecentSearchRepository +import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository +import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase -import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsCountUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase -import com.google.samples.apps.nowinandroid.core.result.Result -import com.google.samples.apps.nowinandroid.core.result.asResult +import com.google.samples.apps.nowinandroid.core.model.data.UserSearchResult import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -41,9 +42,10 @@ import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( getSearchContentsUseCase: GetSearchContentsUseCase, - getSearchContentsCountUseCase: GetSearchContentsCountUseCase, recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase, + private val searchContentsRepository: SearchContentsRepository, private val recentSearchRepository: RecentSearchRepository, + private val userDataRepository: UserDataRepository, private val savedStateHandle: SavedStateHandle, private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { @@ -51,7 +53,7 @@ class SearchViewModel @Inject constructor( val searchQuery = savedStateHandle.getStateFlow(key = SEARCH_QUERY, initialValue = "") val searchResultUiState: StateFlow = - getSearchContentsCountUseCase() + searchContentsRepository.getSearchContentsCount() .flatMapLatest { totalCount -> if (totalCount < SEARCH_MIN_FTS_ENTITY_COUNT) { flowOf(SearchResultUiState.SearchNotReady) @@ -61,18 +63,15 @@ class SearchViewModel @Inject constructor( flowOf(SearchResultUiState.EmptyQuery) } else { getSearchContentsUseCase(query) - .asResult() - .map { result -> - when (result) { - is Result.Success -> SearchResultUiState.Success( - topics = result.data.topics, - newsResources = result.data.newsResources, - ) - - is Result.Loading -> SearchResultUiState.Loading - is Result.Error -> SearchResultUiState.LoadFailed - } + // Not using .asResult() here, because it emits Loading state every + // time the user types a letter in the search box, which flickers the screen. + .map { data -> + SearchResultUiState.Success( + topics = data.topics, + newsResources = data.newsResources, + ) } + .catch { emit(SearchResultUiState.LoadFailed) } } } } @@ -114,6 +113,24 @@ class SearchViewModel @Inject constructor( recentSearchRepository.clearRecentSearches() } } + + fun setNewsResourceBookmarked(newsResourceId: String, isChecked: Boolean) { + viewModelScope.launch { + userDataRepository.setNewsResourceBookmarked(newsResourceId, isChecked) + } + } + + fun followTopic(followedTopicId: String, followed: Boolean) { + viewModelScope.launch { + userDataRepository.setTopicIdFollowed(followedTopicId, followed) + } + } + + fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) { + viewModelScope.launch { + userDataRepository.setNewsResourceViewed(newsResourceId, viewed) + } + } } private fun AnalyticsHelper.logEventSearchTriggered(query: String) = diff --git a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt b/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt index da0d5654e..c832401de 100644 --- a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt +++ b/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt @@ -19,7 +19,6 @@ package com.google.samples.apps.nowinandroid.feature.search import androidx.lifecycle.SavedStateHandle import com.google.samples.apps.nowinandroid.core.analytics.NoOpAnalyticsHelper import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase -import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsCountUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData import com.google.samples.apps.nowinandroid.core.testing.data.topicsTestData @@ -33,6 +32,7 @@ import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.E import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.Loading import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.SearchNotReady import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -59,17 +59,18 @@ class SearchViewModelTest { ) private val recentSearchRepository = TestRecentSearchRepository() private val getRecentQueryUseCase = GetRecentSearchQueriesUseCase(recentSearchRepository) - private val getSearchContentsCountUseCase = GetSearchContentsCountUseCase(searchContentsRepository) + private lateinit var viewModel: SearchViewModel @Before fun setup() { viewModel = SearchViewModel( getSearchContentsUseCase = getSearchContentsUseCase, - getSearchContentsCountUseCase = getSearchContentsCountUseCase, recentSearchQueriesUseCase = getRecentQueryUseCase, + searchContentsRepository = searchContentsRepository, savedStateHandle = SavedStateHandle(), recentSearchRepository = recentSearchRepository, + userDataRepository = userDataRepository, analyticsHelper = NoOpAnalyticsHelper(), ) userDataRepository.setUserData(emptyUserData) @@ -84,7 +85,8 @@ class SearchViewModelTest { fun stateIsEmptyQuery_withEmptySearchQuery() = runTest { searchContentsRepository.addNewsResources(newsResourcesTestData) searchContentsRepository.addTopics(topicsTestData) - val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() } + val collectJob = + launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() } viewModel.onSearchQueryChanged("") @@ -95,7 +97,8 @@ class SearchViewModelTest { @Test fun emptyResultIsReturned_withNotMatchingQuery() = runTest { - val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() } + val collectJob = + launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() } viewModel.onSearchQueryChanged("XXX") searchContentsRepository.addNewsResources(newsResourcesTestData) @@ -109,7 +112,8 @@ class SearchViewModelTest { @Test fun recentSearches_verifyUiStateIsSuccess() = runTest { - val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.recentSearchQueriesUiState.collect() } + val collectJob = + launch(UnconfinedTestDispatcher()) { viewModel.recentSearchQueriesUiState.collect() } viewModel.onSearchTriggered("kotlin") val result = viewModel.recentSearchQueriesUiState.value @@ -120,7 +124,8 @@ class SearchViewModelTest { @Test fun searchNotReady_withNoFtsTableEntity() = runTest { - val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() } + val collectJob = + launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() } viewModel.onSearchQueryChanged("") @@ -128,4 +133,22 @@ class SearchViewModelTest { collectJob.cancel() } + + @Test + fun whenToggleNewsResourceSavedIsCalled_bookmarkStateIsUpdated() = runTest { + val newsResourceId = "123" + viewModel.setNewsResourceBookmarked(newsResourceId, true) + + assertEquals( + expected = setOf(newsResourceId), + actual = userDataRepository.userData.first().bookmarkedNewsResources, + ) + + viewModel.setNewsResourceBookmarked(newsResourceId, false) + + assertEquals( + expected = emptySet(), + actual = userDataRepository.userData.first().bookmarkedNewsResources, + ) + } } diff --git a/feature/settings/README.md b/feature/settings/README.md new file mode 100644 index 000000000..7a4df04fe --- /dev/null +++ b/feature/settings/README.md @@ -0,0 +1,3 @@ +# :feature:settings module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_feature_settings.svg) diff --git a/feature/topic/README.md b/feature/topic/README.md index d74517e63..84588929c 100644 --- a/feature/topic/README.md +++ b/feature/topic/README.md @@ -1,3 +1,3 @@ # :feature:topic module - -![Dependency graph](../../docs/images/graphs/dep_graph_feature_topic.png) +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_feature_topic.svg) diff --git a/feature/topic/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt b/feature/topic/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt index b64e397ea..2b87baf9e 100644 --- a/feature/topic/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt +++ b/feature/topic/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt @@ -55,6 +55,7 @@ class TopicScreenTest { TopicScreen( topicUiState = TopicUiState.Loading, newsUiState = NewsUiState.Loading, + showBackButton = true, onBackClick = {}, onFollowClick = {}, onTopicClick = {}, @@ -75,6 +76,7 @@ class TopicScreenTest { TopicScreen( topicUiState = TopicUiState.Success(testTopic), newsUiState = NewsUiState.Loading, + showBackButton = true, onBackClick = {}, onFollowClick = {}, onTopicClick = {}, @@ -100,6 +102,7 @@ class TopicScreenTest { TopicScreen( topicUiState = TopicUiState.Loading, newsUiState = NewsUiState.Success(userNewsResourcesTestData), + showBackButton = true, onBackClick = {}, onFollowClick = {}, onTopicClick = {}, @@ -123,6 +126,7 @@ class TopicScreenTest { newsUiState = NewsUiState.Success( userNewsResourcesTestData, ), + showBackButton = true, onBackClick = {}, onFollowClick = {}, onTopicClick = {}, diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicDetailPlaceholder.kt b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicDetailPlaceholder.kt new file mode 100644 index 000000000..627fb8fb3 --- /dev/null +++ b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicDetailPlaceholder.kt @@ -0,0 +1,71 @@ +/* + * 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.feature.topic + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +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 + +@Composable +fun TopicDetailPlaceholder(modifier: Modifier = Modifier) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + shape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp), + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy( + 20.dp, + alignment = Alignment.CenterVertically, + ), + ) { + Icon( + painter = painterResource(id = R.drawable.feature_topic_ic_topic_placeholder), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(id = R.string.feature_topic_select_an_interest), + style = MaterialTheme.typography.titleLarge, + ) + } + } +} + +@Preview(widthDp = 200, heightDp = 300) +@Composable +fun TopicDetailPlaceholderPreview() { + NiaTheme { + TopicDetailPlaceholder() + } +} diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index 3f3862c2a..5ac766675 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsTopHeight @@ -44,6 +45,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -70,19 +72,21 @@ import com.google.samples.apps.nowinandroid.feature.topic.R.string @Composable internal fun TopicRoute( + showBackButton: Boolean, onBackClick: () -> Unit, onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, viewModel: TopicViewModel = hiltViewModel(), ) { val topicUiState: TopicUiState by viewModel.topicUiState.collectAsStateWithLifecycle() - val newsUiState: NewsUiState by viewModel.newUiState.collectAsStateWithLifecycle() + val newsUiState: NewsUiState by viewModel.newsUiState.collectAsStateWithLifecycle() TrackScreenViewEvent(screenName = "Topic: ${viewModel.topicId}") TopicScreen( topicUiState = topicUiState, newsUiState = newsUiState, - modifier = modifier, + modifier = modifier.testTag("topic:${viewModel.topicId}"), + showBackButton = showBackButton, onBackClick = onBackClick, onFollowClick = viewModel::followTopicToggle, onBookmarkChanged = viewModel::bookmarkNews, @@ -96,6 +100,7 @@ internal fun TopicRoute( internal fun TopicScreen( topicUiState: TopicUiState, newsUiState: NewsUiState, + showBackButton: Boolean, onBackClick: () -> Unit, onFollowClick: (Boolean) -> Unit, onTopicClick: (String) -> Unit, @@ -127,6 +132,7 @@ internal fun TopicScreen( is TopicUiState.Success -> { item { TopicToolbar( + showBackButton = showBackButton, onBackClick = onBackClick, onFollowClick = onFollowClick, uiState = topicUiState.followableTopic, @@ -270,6 +276,7 @@ private fun TopicBodyPreview() { private fun TopicToolbar( uiState: FollowableTopic, modifier: Modifier = Modifier, + showBackButton: Boolean = true, onBackClick: () -> Unit = {}, onFollowClick: (Boolean) -> Unit = {}, ) { @@ -280,13 +287,18 @@ private fun TopicToolbar( .fillMaxWidth() .padding(bottom = 32.dp), ) { - IconButton(onClick = { onBackClick() }) { - Icon( - imageVector = NiaIcons.ArrowBack, - contentDescription = stringResource( - id = com.google.samples.apps.nowinandroid.core.ui.R.string.core_ui_back, - ), - ) + if (showBackButton) { + IconButton(onClick = { onBackClick() }) { + Icon( + imageVector = NiaIcons.ArrowBack, + contentDescription = stringResource( + id = com.google.samples.apps.nowinandroid.core.ui.R.string.core_ui_back, + ), + ) + } + } else { + // Keeps the NiaFilterChip aligned to the end of the Row. + Spacer(modifier = Modifier.width(1.dp)) } val selected = uiState.isFollowed NiaFilterChip( @@ -314,6 +326,7 @@ fun TopicScreenPopulated( TopicScreen( topicUiState = TopicUiState.Success(userNewsResources[0].followableTopics[0]), newsUiState = NewsUiState.Success(userNewsResources), + showBackButton = true, onBackClick = {}, onFollowClick = {}, onBookmarkChanged = { _, _ -> }, @@ -332,6 +345,7 @@ fun TopicScreenLoading() { TopicScreen( topicUiState = TopicUiState.Loading, newsUiState = NewsUiState.Loading, + showBackButton = true, onBackClick = {}, onFollowClick = {}, onBookmarkChanged = { _, _ -> }, diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt index ff1eee319..255e40f8b 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt @@ -62,7 +62,7 @@ class TopicViewModel @Inject constructor( initialValue = TopicUiState.Loading, ) - val newUiState: StateFlow = newsUiState( + val newsUiState: StateFlow = newsUiState( topicId = topicArgs.topicId, userDataRepository = userDataRepository, userNewsResourceRepository = userNewsResourceRepository, @@ -81,7 +81,7 @@ class TopicViewModel @Inject constructor( fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) { viewModelScope.launch { - userDataRepository.updateNewsResourceBookmark(newsResourceId, bookmarked) + userDataRepository.setNewsResourceBookmarked(newsResourceId, bookmarked) } } 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 index bba46c5ab..41804b634 100644 --- 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 @@ -20,6 +20,7 @@ import androidx.annotation.VisibleForTesting import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument @@ -32,20 +33,23 @@ private val URL_CHARACTER_ENCODING = UTF_8.name() @VisibleForTesting internal const val TOPIC_ID_ARG = "topicId" +const val TOPIC_ROUTE = "topic_route" internal class TopicArgs(val topicId: String) { constructor(savedStateHandle: SavedStateHandle) : this(URLDecoder.decode(checkNotNull(savedStateHandle[TOPIC_ID_ARG]), URL_CHARACTER_ENCODING)) } -fun NavController.navigateToTopic(topicId: String) { +fun NavController.navigateToTopic(topicId: String, navOptions: NavOptionsBuilder.() -> Unit = {}) { val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING) - navigate("topic_route/$encodedId") { - launchSingleTop = true + val newRoute = "$TOPIC_ROUTE/$encodedId" + navigate(newRoute) { + navOptions() } } fun NavGraphBuilder.topicScreen( + showBackButton: Boolean, onBackClick: () -> Unit, onTopicClick: (String) -> Unit, ) { @@ -55,6 +59,10 @@ fun NavGraphBuilder.topicScreen( navArgument(TOPIC_ID_ARG) { type = NavType.StringType }, ), ) { - TopicRoute(onBackClick = onBackClick, onTopicClick = onTopicClick) + TopicRoute( + showBackButton = showBackButton, + onBackClick = onBackClick, + onTopicClick = onTopicClick, + ) } } diff --git a/feature/topic/src/main/res/drawable/feature_topic_ic_topic_placeholder.xml b/feature/topic/src/main/res/drawable/feature_topic_ic_topic_placeholder.xml new file mode 100644 index 000000000..0518401da --- /dev/null +++ b/feature/topic/src/main/res/drawable/feature_topic_ic_topic_placeholder.xml @@ -0,0 +1,55 @@ + + + + + + + + + + diff --git a/feature/topic/src/main/res/values/strings.xml b/feature/topic/src/main/res/values/strings.xml index 5fefc3f42..fe4a6dc29 100644 --- a/feature/topic/src/main/res/values/strings.xml +++ b/feature/topic/src/main/res/values/strings.xml @@ -16,4 +16,5 @@ --> Loading topic + Select an Interest diff --git a/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt b/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt index a9c9d96dc..565732f59 100644 --- a/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt +++ b/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt @@ -91,7 +91,7 @@ class TopicViewModelTest { @Test fun uiStateNews_whenInitialized_thenShowLoading() = runTest { - assertEquals(NewsUiState.Loading, viewModel.newUiState.value) + assertEquals(NewsUiState.Loading, viewModel.newsUiState.value) } @Test @@ -117,7 +117,7 @@ class TopicViewModelTest { topicsRepository.sendTopics(testInputTopics.map { it.topic }) userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id)) val topicUiState = viewModel.topicUiState.value - val newsUiState = viewModel.newUiState.value + val newsUiState = viewModel.newsUiState.value assertIs(topicUiState) assertIs(newsUiState) @@ -131,7 +131,7 @@ class TopicViewModelTest { val collectJob = launch(UnconfinedTestDispatcher()) { combine( viewModel.topicUiState, - viewModel.newUiState, + viewModel.newsUiState, ::Pair, ).collect() } @@ -139,7 +139,7 @@ class TopicViewModelTest { userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id)) newsRepository.sendNewsResources(sampleNewsResources) val topicUiState = viewModel.topicUiState.value - val newsUiState = viewModel.newUiState.value + val newsUiState = viewModel.newsUiState.value assertIs(topicUiState) assertIs(newsUiState) diff --git a/generateModuleGraphs.sh b/generateModuleGraphs.sh new file mode 100755 index 000000000..fb2d74712 --- /dev/null +++ b/generateModuleGraphs.sh @@ -0,0 +1,108 @@ +#!/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-get install graphviz'" + 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=$(./gradlew -q printModulePaths --no-configuration-cache) + +# 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 + # "docs/images/graphs/${file_name}.svg" + # Remove the temporary .gv file + rm "/tmp/${file_name}.gv" + fi +done \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index c0acfeb02..97f940e2e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,3 +39,6 @@ kotlin.code.style=official # https://developer.android.com/build/releases/gradle-plugin#default-changes android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false + +# Run Roborazzi screenshot tests with the local tests +roborazzi.test.verify=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c4ff333a..29daf9a70 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,22 @@ [versions] -accompanist = "0.32.0" +accompanist = "0.34.0" androidDesugarJdkLibs = "2.0.4" # AGP and tools should be updated together androidGradlePlugin = "8.3.0" androidTools = "31.3.0" androidxActivity = "1.8.0" androidxAppCompat = "1.6.1" -androidxBrowser = "1.6.0" -androidxComposeBom = "2024.02.00" +androidxBrowser = "1.8.0" +androidxComposeBom = "2024.02.02" androidxComposeCompiler = "1.5.8" androidxComposeUiTest = "1.7.0-alpha05" +androidxComposeMaterial3Adaptive = "1.0.0-alpha08" androidxComposeRuntimeTracing = "1.0.0-beta01" androidxCore = "1.12.0" androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.0.0" androidxEspresso = "3.5.1" -androidxHiltNavigationCompose = "1.0.0" +androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.7.0" androidxMacroBenchmark = "1.2.2" androidxMetrics = "1.0.0-alpha04" @@ -29,12 +30,12 @@ androidxTracing = "1.3.0-alpha02" androidxUiAutomator = "2.2.0" androidxWindowManager = "1.2.0" androidxWork = "2.9.0" -coil = "2.5.0" +coil = "2.6.0" dependencyGuard = "0.4.3" firebaseBom = "32.4.0" firebaseCrashlyticsPlugin = "2.9.9" firebasePerfPlugin = "1.4.2" -gmsPlugin = "4.4.0" +gmsPlugin = "4.4.1" googleOss = "17.0.1" googleOssPlugin = "0.10.6" hilt = "2.51" @@ -42,10 +43,11 @@ hiltExt = "1.1.0" jacoco = "0.8.7" junit4 = "4.13.2" kotlin = "1.9.22" -kotlinxCoroutines = "1.7.3" +kotlinxCoroutines = "1.8.0" kotlinxDatetime = "0.5.0" -kotlinxSerializationJson = "1.6.0" +kotlinxSerializationJson = "1.6.3" ksp = "1.9.22-1.0.18" +moduleGraph = "2.5.0" okhttp = "4.12.0" protobuf = "3.25.2" protobufPlugin = "0.9.4" @@ -70,6 +72,9 @@ androidx-compose-foundation = { group = "androidx.compose.foundation", name = "f androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "androidxComposeMaterial3Adaptive" } +androidx-compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "androidxComposeMaterial3Adaptive" } +androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "androidxComposeMaterial3Adaptive" } androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class" } androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidxComposeRuntimeTracing" } @@ -158,6 +163,7 @@ hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +module-graph = { id = "com.jraska.module.graph.assertion", version.ref = "moduleGraph" } protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } room = { id = "androidx.room", version.ref = "room" } diff --git a/lint/README.md b/lint/README.md deleted file mode 100644 index 3eceb434b..000000000 --- a/lint/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# :lint module - -![Dependency graph](../docs/images/graphs/dep_graph_lint.png) diff --git a/settings.gradle.kts b/settings.gradle.kts index 949dbfdd1..d4b6654b1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -63,3 +63,4 @@ include(":lint") include(":sync:work") include(":sync:sync-test") include(":ui-test-hilt-manifest") + diff --git a/sync/README.md b/sync/README.md deleted file mode 100644 index b100e27ad..000000000 --- a/sync/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# :sync module - -![Dependency graph](../docs/images/graphs/dep_graph_sync.png) diff --git a/sync/sync-test/README.md b/sync/sync-test/README.md new file mode 100644 index 000000000..78876290f --- /dev/null +++ b/sync/sync-test/README.md @@ -0,0 +1,3 @@ +# :sync:sync-test module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_sync_sync_test.svg) diff --git a/sync/work/README.md b/sync/work/README.md new file mode 100644 index 000000000..2fe66d616 --- /dev/null +++ b/sync/work/README.md @@ -0,0 +1,3 @@ +# :sync:work module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_sync_work.svg)