Merge branch 'main' into firebase-ktx

pull/1837/head
Simon Marquis 2 years ago committed by GitHub
commit c3bb7ce4b5

@ -4,3 +4,4 @@
[*.{kt,kts}] [*.{kt,kts}]
ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true ij_kotlin_allow_trailing_comma_on_call_site=true
ktlint_function_naming_ignore_when_annotated_with=Composable, Test

@ -18,6 +18,7 @@ updates:
- "org.jetbrains.kotlin.jvm" - "org.jetbrains.kotlin.jvm"
- "com.google.devtools.ksp" - "com.google.devtools.ksp"
- "androidx.compose.compiler:compiler" - "androidx.compose.compiler:compiler"
open-pull-requests-limit: 10
registries: registries:
maven-google: maven-google:
type: "maven-repository" type: "maven-repository"

@ -31,7 +31,7 @@ jobs:
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 17 java-version: 17
@ -75,7 +75,7 @@ jobs:
# Run local tests after screenshot tests to avoid wrong UP-TO-DATE. TODO: Ignore screenshots. # Run local tests after screenshot tests to avoid wrong UP-TO-DATE. TODO: Ignore screenshots.
- name: Run local tests - name: Run local tests
if: always() if: always()
run: ./gradlew testDemoDebug testProdDebug :lint:test run: ./gradlew testDemoDebug :lint:test
# Replace task exclusions with `-Pandroidx.baselineprofile.skipgeneration` when # Replace task exclusions with `-Pandroidx.baselineprofile.skipgeneration` when
# https://android-review.googlesource.com/c/platform/frameworks/support/+/2602790 landed in a # https://android-review.googlesource.com/c/platform/frameworks/support/+/2602790 landed in a
# release build # release build
@ -91,14 +91,14 @@ jobs:
-x collectProdNonMinifiedBenchmarkBaselineProfile -x collectProdNonMinifiedBenchmarkBaselineProfile
- name: Upload build outputs (APKs) - name: Upload build outputs (APKs)
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: APKs name: APKs
path: '**/build/outputs/apk/**/*.apk' path: '**/build/outputs/apk/**/*.apk'
- name: Upload test results (XML) - name: Upload test results (XML)
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: test-results name: test-results
path: '**/build/test-results/test*UnitTest/**.xml' path: '**/build/test-results/test*UnitTest/**.xml'
@ -108,7 +108,7 @@ jobs:
- name: Upload lint reports (HTML) - name: Upload lint reports (HTML)
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: lint-reports name: lint-reports
path: '**/build/reports/lint-results-*.html' path: '**/build/reports/lint-results-*.html'
@ -131,7 +131,7 @@ jobs:
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 17 java-version: 17
@ -154,7 +154,7 @@ jobs:
- name: Upload test reports - name: Upload test reports
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: test-reports-${{ matrix.api-level }} name: test-reports-${{ matrix.api-level }}
path: '**/build/reports/androidTests' path: '**/build/reports/androidTests'

@ -21,7 +21,7 @@ jobs:
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 17 java-version: 17

@ -22,7 +22,7 @@ The app is currently in development. The `prodRelease` variant is [available on
**Now in Android** displays content from the **Now in Android** displays content from the
[Now in Android](https://developer.android.com/series/now-in-android) series. Users can browse for [Now in Android](https://developer.android.com/series/now-in-android) series. Users can browse for
links to recent videos, articles and other content. Users can also follow topics they are interested links to recent videos, articles and other content. Users can also follow topics they are interested
in. in, and be notified when new content is published which matches interests they are following.
## Screenshots ## Screenshots
@ -109,12 +109,42 @@ Examples:
manipulate the state of the `Test` repository and verify the resulting behavior, instead of manipulate the state of the `Test` repository and verify the resulting behavior, instead of
checking that specific repository methods were called. checking that specific repository methods were called.
## Screenshot tests To run the tests execute the following gradle tasks:
- `testDemoDebug` run all local tests against the `demoDebug` variant.
- `connectedDemoDebugAndroidTest` run all instrumented tests against the `demoDebug` variant.
**Now In Android** uses [Roborazzi](https://github.com/takahirom/roborazzi) to do screenshot tests **Note:** You should not run `./gradlew test` or `./gradlew connectedAndroidTest` as this will execute
of certain screens and components. To run these tests, run the `verifyRoborazziDemoDebug` or tests against _all_ build variants which is both unecessary and will result in failures as only the
`recordRoborazziDemoDebug` tasks. Note that screenshots are recorded on CI, using Linux, and other `demoDebug` variant is supported. No other variants have any tests (although this might change in future).
platforms might generate slightly different images, making the tests fail.
## Screenshot tests
A screenshot test takes a screenshot of a screen or a UI component within the app, and compares it
with a previously recorded screenshot which is known to be rendered correctly.
For example, Now in Android has [screenshot tests](https://github.com/android/nowinandroid/blob/main/app/src/testDemoDebug/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt)
to verify that the navigation is displayed correctly on different screen sizes
([known correct screenshots](https://github.com/android/nowinandroid/tree/main/app/src/testDemoDebug/screenshots)).
Now In Android uses [Roborazzi](https://github.com/takahirom/roborazzi) to run screenshot tests
of certain screens and UI components. When working with screenshot tests the following gradle tasks are useful:
- `verifyRoborazziDemoDebug` run all screenshot tests, verifying the screenshots against the known
correct screenshots.
- `recordRoborazziDemoDebug` record new "known correct" screenshots. Use this command when you have
made changes to the UI and manually verified that they are rendered correctly. Screenshots will be
stored in `modulename/src/test/screenshots`.
- `compareRoborazziDemoDebug` create comparison images between failed tests and the known correct
images. These can also be found in `modulename/src/test/screenshots`.
**Note:** The known correct screenshots stored in this repository are recorded on CI using Linux. Other
platforms may (and probably will) generate slightly different images, making the screenshot tests fail.
When working on a non-Linux platform, a workaround to this is to run `recordRoborazziDemoDebug` on the
`main` branch before starting work. After making changes, `verifyRoborazziDemoDebug` will identify only
legitimate changes.
For more information about screenshot testing
[check out this talk](https://www.droidcon.com/2023/11/15/easy-screenshot-testing-with-compose/).
# UI # UI
The app was designed using [Material 3 guidelines](https://m3.material.io/). Learn more about the design process and The app was designed using [Material 3 guidelines](https://m3.material.io/). Learn more about the design process and

@ -65,9 +65,10 @@ android {
} }
dependencies { dependencies {
implementation(libs.androidx.activity.compose)
implementation(projects.core.designsystem) implementation(projects.core.designsystem)
implementation(projects.core.ui) implementation(projects.core.ui)
implementation(libs.androidx.activity.compose)
} }
dependencyGuard { dependencyGuard {

@ -1,15 +1,16 @@
androidx.activity:activity-compose:1.8.0 androidx.activity:activity-compose:1.8.0
androidx.activity:activity-ktx:1.8.0 androidx.activity:activity-ktx:1.8.0
androidx.activity:activity:1.8.0 androidx.activity:activity:1.8.0
androidx.annotation:annotation-experimental:1.3.0 androidx.annotation:annotation-experimental:1.3.1
androidx.annotation:annotation-jvm:1.6.0 androidx.annotation:annotation-jvm:1.7.0
androidx.annotation:annotation:1.6.0 androidx.annotation:annotation:1.7.0
androidx.appcompat:appcompat-resources:1.6.1 androidx.appcompat:appcompat-resources:1.6.1
androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0 androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0 androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.6.0 androidx.browser:browser:1.6.0
androidx.collection:collection:1.2.0 androidx.collection:collection-jvm:1.3.0
androidx.collection:collection:1.3.0
androidx.compose.animation:animation-android:1.5.4 androidx.compose.animation:animation-android:1.5.4
androidx.compose.animation:animation-core-android:1.5.4 androidx.compose.animation:animation-core-android:1.5.4
androidx.compose.animation:animation-core:1.5.4 androidx.compose.animation:animation-core:1.5.4
@ -26,7 +27,6 @@ androidx.compose.material:material-icons-extended:1.5.4
androidx.compose.material:material-ripple-android:1.5.4 androidx.compose.material:material-ripple-android:1.5.4
androidx.compose.material:material-ripple:1.5.4 androidx.compose.material:material-ripple:1.5.4
androidx.compose.runtime:runtime-android:1.5.4 androidx.compose.runtime:runtime-android:1.5.4
androidx.compose.runtime:runtime-livedata:1.5.4
androidx.compose.runtime:runtime-saveable-android:1.5.4 androidx.compose.runtime:runtime-saveable-android:1.5.4
androidx.compose.runtime:runtime-saveable:1.5.4 androidx.compose.runtime:runtime-saveable:1.5.4
androidx.compose.runtime:runtime:1.5.4 androidx.compose.runtime:runtime:1.5.4
@ -50,118 +50,54 @@ androidx.core:core-ktx:1.12.0
androidx.core:core:1.12.0 androidx.core:core:1.12.0
androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0
androidx.customview:customview:1.0.0 androidx.customview:customview:1.0.0
androidx.datastore:datastore-core:1.0.0
androidx.datastore:datastore:1.0.0
androidx.documentfile:documentfile:1.0.0
androidx.emoji2:emoji2:1.4.0 androidx.emoji2:emoji2:1.4.0
androidx.exifinterface:exifinterface:1.3.6 androidx.exifinterface:exifinterface:1.3.6
androidx.fragment:fragment:1.5.1 androidx.fragment:fragment:1.5.1
androidx.interpolator:interpolator:1.0.0 androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0 androidx.lifecycle:lifecycle-common-java8:2.6.2
androidx.lifecycle:lifecycle-common-java8:2.6.1 androidx.lifecycle:lifecycle-common:2.6.2
androidx.lifecycle:lifecycle-common:2.6.1 androidx.lifecycle:lifecycle-livedata-core:2.6.2
androidx.lifecycle:lifecycle-livedata-core:2.6.1 androidx.lifecycle:lifecycle-livedata:2.6.2
androidx.lifecycle:lifecycle-livedata:2.6.1 androidx.lifecycle:lifecycle-process:2.6.2
androidx.lifecycle:lifecycle-process:2.6.1 androidx.lifecycle:lifecycle-runtime-ktx:2.6.2
androidx.lifecycle:lifecycle-runtime-ktx:2.6.1 androidx.lifecycle:lifecycle-runtime:2.6.2
androidx.lifecycle:lifecycle-runtime:2.6.1 androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2
androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1 androidx.lifecycle:lifecycle-viewmodel:2.6.2
androidx.lifecycle:lifecycle-viewmodel:2.6.1
androidx.loader:loader:1.0.0 androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.metrics:metrics-performance:1.0.0-alpha04 androidx.metrics:metrics-performance:1.0.0-alpha04
androidx.print:print:1.0.0
androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05
androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05
androidx.profileinstaller:profileinstaller:1.3.1 androidx.profileinstaller:profileinstaller:1.3.1
androidx.room:room-common:2.6.0
androidx.room:room-ktx:2.6.0
androidx.room:room-runtime:2.6.0
androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate-ktx:1.2.1
androidx.savedstate:savedstate:1.2.1 androidx.savedstate:savedstate:1.2.1
androidx.sqlite:sqlite-framework:2.4.0
androidx.sqlite:sqlite:2.4.0
androidx.startup:startup-runtime:1.1.1 androidx.startup:startup-runtime:1.1.1
androidx.tracing:tracing-ktx:1.1.0 androidx.tracing:tracing:1.0.0
androidx.tracing:tracing:1.1.0
androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable-animated:1.1.0
androidx.vectordrawable:vectordrawable:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager:viewpager:1.0.0 androidx.viewpager:viewpager:1.0.0
com.caverock:androidsvg-aar:1.4 com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.accompanist:accompanist-drawablepainter:0.30.1
com.google.android.datatransport:transport-api:3.0.0
com.google.android.datatransport:transport-backend-cct:3.1.8
com.google.android.datatransport:transport-runtime:3.1.8
com.google.android.gms:play-services-ads-identifier:18.0.0
com.google.android.gms:play-services-base:18.0.1
com.google.android.gms:play-services-basement:18.1.0
com.google.android.gms:play-services-cloud-messaging:17.0.1
com.google.android.gms:play-services-measurement-api:21.4.0
com.google.android.gms:play-services-measurement-base:21.4.0
com.google.android.gms:play-services-measurement-impl:21.4.0
com.google.android.gms:play-services-measurement-sdk-api:21.4.0
com.google.android.gms:play-services-measurement-sdk:21.4.0
com.google.android.gms:play-services-measurement:21.4.0
com.google.android.gms:play-services-stats:17.0.2
com.google.android.gms:play-services-tasks:18.0.2
com.google.code.findbugs:jsr305:3.0.2 com.google.code.findbugs:jsr305:3.0.2
com.google.dagger:dagger-lint-aar:2.48.1 com.google.dagger:dagger-lint-aar:2.50
com.google.dagger:dagger:2.48.1 com.google.dagger:dagger:2.50
com.google.dagger:hilt-android:2.48.1 com.google.dagger:hilt-android:2.50
com.google.dagger:hilt-core:2.48.1 com.google.dagger:hilt-core:2.50
com.google.errorprone:error_prone_annotations:2.11.0 com.google.guava:listenablefuture:1.0
com.google.firebase:firebase-analytics-ktx:21.4.0
com.google.firebase:firebase-analytics:21.4.0
com.google.firebase:firebase-annotations:16.2.0
com.google.firebase:firebase-bom:32.4.0
com.google.firebase:firebase-common-ktx:20.4.2
com.google.firebase:firebase-common:20.4.2
com.google.firebase:firebase-components:17.1.5
com.google.firebase:firebase-datatransport:18.1.7
com.google.firebase:firebase-encoders-json:18.0.0
com.google.firebase:firebase-encoders-proto:16.0.0
com.google.firebase:firebase-encoders:17.0.0
com.google.firebase:firebase-iid-interop:17.1.0
com.google.firebase:firebase-installations-interop:17.1.1
com.google.firebase:firebase-installations:17.2.0
com.google.firebase:firebase-measurement-connector:19.0.0
com.google.firebase:firebase-messaging-ktx:23.3.0
com.google.firebase:firebase-messaging:23.3.0
com.google.guava:failureaccess:1.0.1
com.google.guava:guava:31.1-android
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
com.google.j2objc:j2objc-annotations:1.3
com.google.protobuf:protobuf-javalite:3.24.4
com.google.protobuf:protobuf-kotlin-lite:3.24.4
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.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.6.0 com.squareup.okio:okio-jvm:3.6.0
com.squareup.okio:okio:3.6.0 com.squareup.okio:okio:3.6.0
com.squareup.retrofit2:retrofit:2.9.0 io.coil-kt:coil-base:2.5.0
io.coil-kt:coil-base:2.4.0 io.coil-kt:coil-compose-base:2.5.0
io.coil-kt:coil-compose-base:2.4.0 io.coil-kt:coil-compose:2.5.0
io.coil-kt:coil-compose:2.4.0 io.coil-kt:coil:2.5.0
io.coil-kt:coil-svg:2.4.0
io.coil-kt:coil:2.4.0
javax.inject:javax.inject:1 javax.inject:javax.inject:1
org.checkerframework:checker-qual:3.12.0 org.jetbrains.kotlin:kotlin-stdlib-common:1.9.21
org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10
org.jetbrains.kotlin:kotlin-stdlib:1.9.10 org.jetbrains.kotlin:kotlin-stdlib:1.9.21
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-bom: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-jvm:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3 org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.5.0
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.4.1 org.jetbrains.kotlinx:kotlinx-datetime:0.5.0
org.jetbrains.kotlinx:kotlinx-datetime:0.4.1
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:annotations:23.0.0 org.jetbrains:annotations:23.0.0

@ -25,6 +25,7 @@ plugins {
alias(libs.plugins.nowinandroid.android.application.firebase) alias(libs.plugins.nowinandroid.android.application.firebase)
id("com.google.android.gms.oss-licenses-plugin") id("com.google.android.gms.oss-licenses-plugin")
alias(libs.plugins.baselineprofile) alias(libs.plugins.baselineprofile)
alias(libs.plugins.roborazzi)
} }
android { android {
@ -96,47 +97,41 @@ dependencies {
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.core.model) implementation(projects.core.model)
implementation(projects.core.analytics) implementation(projects.core.analytics)
implementation(projects.sync.work) implementation(projects.sync.work)
androidTestImplementation(projects.core.testing)
androidTestImplementation(projects.core.datastoreTest)
androidTestImplementation(projects.core.dataTest)
androidTestImplementation(projects.core.network)
androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.accompanist.testharness)
androidTestImplementation(kotlin("test"))
debugImplementation(libs.androidx.compose.ui.testManifest)
debugImplementation(projects.uiTestHiltManifest)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.compose.runtime) implementation(libs.androidx.tracing.ktx)
implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.compose.runtime.tracing)
implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.compose.material3.windowSizeClass)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.window.manager)
implementation(libs.androidx.profileinstaller) implementation(libs.androidx.profileinstaller)
implementation(libs.kotlinx.coroutines.guava) implementation(libs.kotlinx.coroutines.guava)
implementation(libs.coil.kt) implementation(libs.coil.kt)
baselineProfile(project(":benchmarks")) debugImplementation(libs.androidx.compose.ui.testManifest)
debugImplementation(projects.uiTestHiltManifest)
kspTest(libs.hilt.compiler)
// Core functions
testImplementation(projects.core.testing)
testImplementation(projects.core.datastoreTest)
testImplementation(projects.core.dataTest) testImplementation(projects.core.dataTest)
testImplementation(projects.core.network) testImplementation(projects.core.testing)
testImplementation(libs.androidx.navigation.testing)
testImplementation(libs.accompanist.testharness) testImplementation(libs.accompanist.testharness)
testImplementation(libs.hilt.android.testing)
testImplementation(libs.work.testing) testImplementation(libs.work.testing)
testImplementation(kotlin("test"))
kspTest(libs.hilt.compiler)
testDemoImplementation(libs.robolectric)
testDemoImplementation(libs.roborazzi)
androidTestImplementation(projects.core.testing)
androidTestImplementation(projects.core.dataTest)
androidTestImplementation(projects.core.datastoreTest)
androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.accompanist.testharness)
androidTestImplementation(libs.hilt.android.testing)
baselineProfile(projects.benchmarks)
} }
baselineProfile { baselineProfile {

@ -1,17 +1,18 @@
androidx.activity:activity-compose:1.8.0 androidx.activity:activity-compose:1.8.0
androidx.activity:activity-ktx:1.8.0 androidx.activity:activity-ktx:1.8.0
androidx.activity:activity:1.8.0 androidx.activity:activity:1.8.0
androidx.annotation:annotation-experimental:1.3.0 androidx.annotation:annotation-experimental:1.3.1
androidx.annotation:annotation-jvm:1.6.0 androidx.annotation:annotation-jvm:1.7.0
androidx.annotation:annotation:1.6.0 androidx.annotation:annotation:1.7.0
androidx.appcompat:appcompat-resources:1.6.1 androidx.appcompat:appcompat-resources:1.6.1
androidx.appcompat:appcompat:1.6.1 androidx.appcompat:appcompat:1.6.1
androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0 androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0 androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.6.0 androidx.browser:browser:1.6.0
androidx.collection:collection-ktx:1.1.0 androidx.collection:collection-jvm:1.3.0
androidx.collection:collection:1.2.0 androidx.collection:collection-ktx:1.3.0
androidx.collection:collection:1.3.0
androidx.compose.animation:animation-android:1.5.4 androidx.compose.animation:animation-android:1.5.4
androidx.compose.animation:animation-core-android:1.5.4 androidx.compose.animation:animation-core-android:1.5.4
androidx.compose.animation:animation-core:1.5.4 androidx.compose.animation:animation-core:1.5.4
@ -29,10 +30,8 @@ androidx.compose.material:material-icons-extended:1.5.4
androidx.compose.material:material-ripple-android:1.5.4 androidx.compose.material:material-ripple-android:1.5.4
androidx.compose.material:material-ripple:1.5.4 androidx.compose.material:material-ripple:1.5.4
androidx.compose.runtime:runtime-android:1.5.4 androidx.compose.runtime:runtime-android:1.5.4
androidx.compose.runtime:runtime-livedata:1.5.4
androidx.compose.runtime:runtime-saveable-android:1.5.4 androidx.compose.runtime:runtime-saveable-android:1.5.4
androidx.compose.runtime:runtime-saveable:1.5.4 androidx.compose.runtime:runtime-saveable:1.5.4
androidx.compose.runtime:runtime-tracing:1.0.0-alpha03
androidx.compose.runtime:runtime:1.5.4 androidx.compose.runtime:runtime:1.5.4
androidx.compose.ui:ui-android:1.5.4 androidx.compose.ui:ui-android:1.5.4
androidx.compose.ui:ui-geometry-android:1.5.4 androidx.compose.ui:ui-geometry-android:1.5.4
@ -74,9 +73,7 @@ androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.6.2 androidx.lifecycle:lifecycle-common-java8:2.6.2
androidx.lifecycle:lifecycle-common:2.6.2 androidx.lifecycle:lifecycle-common:2.6.2
androidx.lifecycle:lifecycle-livedata-core-ktx:2.6.2
androidx.lifecycle:lifecycle-livedata-core:2.6.2 androidx.lifecycle:lifecycle-livedata-core:2.6.2
androidx.lifecycle:lifecycle-livedata-ktx:2.6.2
androidx.lifecycle:lifecycle-livedata:2.6.2 androidx.lifecycle:lifecycle-livedata:2.6.2
androidx.lifecycle:lifecycle-process:2.6.2 androidx.lifecycle:lifecycle-process:2.6.2
androidx.lifecycle:lifecycle-runtime-compose:2.6.2 androidx.lifecycle:lifecycle-runtime-compose:2.6.2
@ -100,28 +97,25 @@ androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05
androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05 androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05
androidx.profileinstaller:profileinstaller:1.3.1 androidx.profileinstaller:profileinstaller:1.3.1
androidx.resourceinspection:resourceinspection-annotation:1.0.1 androidx.resourceinspection:resourceinspection-annotation:1.0.1
androidx.room:room-common:2.6.0 androidx.room:room-common:2.6.1
androidx.room:room-ktx:2.6.0 androidx.room:room-ktx:2.6.1
androidx.room:room-runtime:2.6.0 androidx.room:room-runtime:2.6.1
androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate-ktx:1.2.1
androidx.savedstate:savedstate:1.2.1 androidx.savedstate:savedstate:1.2.1
androidx.sqlite:sqlite-framework:2.4.0 androidx.sqlite:sqlite-framework:2.4.0
androidx.sqlite:sqlite:2.4.0 androidx.sqlite:sqlite:2.4.0
androidx.startup:startup-runtime:1.1.1 androidx.startup:startup-runtime:1.1.1
androidx.tracing:tracing-ktx:1.2.0-alpha02 androidx.tracing:tracing-ktx:1.1.0
androidx.tracing:tracing-perfetto-common:1.0.0-alpha11 androidx.tracing:tracing:1.1.0
androidx.tracing:tracing-perfetto:1.0.0-alpha11
androidx.tracing:tracing:1.2.0-alpha02
androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable-animated:1.1.0
androidx.vectordrawable:vectordrawable:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager:viewpager:1.0.0 androidx.viewpager:viewpager:1.0.0
androidx.window.extensions.core:core:1.0.0 androidx.window:window:1.0.0
androidx.window:window:1.1.0
androidx.work:work-runtime-ktx:2.9.0-rc01 androidx.work:work-runtime-ktx:2.9.0-rc01
androidx.work:work-runtime:2.9.0-rc01 androidx.work:work-runtime:2.9.0-rc01
com.caverock:androidsvg-aar:1.4 com.caverock:androidsvg-aar:1.4
com.google.accompanist:accompanist-drawablepainter:0.30.1 com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.accompanist:accompanist-permissions:0.32.0 com.google.accompanist:accompanist-permissions:0.32.0
com.google.android.datatransport:transport-api:3.0.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-backend-cct:3.1.9
@ -140,10 +134,10 @@ com.google.android.gms:play-services-oss-licenses:17.0.1
com.google.android.gms:play-services-stats:17.0.2 com.google.android.gms:play-services-stats:17.0.2
com.google.android.gms:play-services-tasks:18.0.2 com.google.android.gms:play-services-tasks:18.0.2
com.google.code.findbugs:jsr305:3.0.2 com.google.code.findbugs:jsr305:3.0.2
com.google.dagger:dagger-lint-aar:2.48.1 com.google.dagger:dagger-lint-aar:2.50
com.google.dagger:dagger:2.48.1 com.google.dagger:dagger:2.50
com.google.dagger:hilt-android:2.48.1 com.google.dagger:hilt-android:2.50
com.google.dagger:hilt-core:2.48.1 com.google.dagger:hilt-core:2.50
com.google.errorprone:error_prone_annotations:2.11.0 com.google.errorprone:error_prone_annotations:2.11.0
com.google.firebase:firebase-abt:21.1.1 com.google.firebase:firebase-abt:21.1.1
com.google.firebase:firebase-analytics-ktx:21.4.0 com.google.firebase:firebase-analytics-ktx:21.4.0
@ -182,27 +176,27 @@ com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.6.0 com.squareup.okio:okio-jvm:3.6.0
com.squareup.okio:okio:3.6.0 com.squareup.okio:okio:3.6.0
com.squareup.retrofit2:retrofit:2.9.0 com.squareup.retrofit2:retrofit:2.9.0
io.coil-kt:coil-base:2.4.0 io.coil-kt:coil-base:2.5.0
io.coil-kt:coil-compose-base:2.4.0 io.coil-kt:coil-compose-base:2.5.0
io.coil-kt:coil-compose:2.4.0 io.coil-kt:coil-compose:2.5.0
io.coil-kt:coil-svg:2.4.0 io.coil-kt:coil-svg:2.5.0
io.coil-kt:coil:2.4.0 io.coil-kt:coil:2.5.0
io.github.aakira:napier-android:1.4.1 io.github.aakira:napier-android:1.4.1
io.github.aakira:napier:1.4.1 io.github.aakira:napier:1.4.1
javax.inject:javax.inject:1 javax.inject:javax.inject:1
org.checkerframework:checker-qual:3.12.0 org.checkerframework:checker-qual:3.12.0
org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10 org.jetbrains.kotlin:kotlin-stdlib-common:1.9.21
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10
org.jetbrains.kotlin:kotlin-stdlib:1.9.10 org.jetbrains.kotlin:kotlin-stdlib:1.9.21
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-bom: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-jvm:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core: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-guava:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3 org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.4.1 org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.5.0
org.jetbrains.kotlinx:kotlinx-datetime:0.4.1 org.jetbrains.kotlinx:kotlinx-datetime:0.5.0
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.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-jvm:1.6.0
org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0 org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0

@ -107,6 +107,7 @@ application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml' application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml'
application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml' application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml'
launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon='' launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon=''
uses-library-not-required:'android.ext.adservices'
uses-library-not-required:'androidx.window.extensions' uses-library-not-required:'androidx.window.extensions'
uses-library-not-required:'androidx.window.sidecar' uses-library-not-required:'androidx.window.sidecar'
property: name='android.adservices.AD_SERVICES_CONFIG' resource='res/xml/ga_ad_services_config.xml' property: name='android.adservices.AD_SERVICES_CONFIG' resource='res/xml/ga_ad_services_config.xml'

@ -51,7 +51,7 @@ import javax.inject.Inject
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR
import com.google.samples.apps.nowinandroid.feature.interests.R as FeatureInterestsR import com.google.samples.apps.nowinandroid.feature.search.R as FeatureSearchR
import com.google.samples.apps.nowinandroid.feature.settings.R as SettingsR import com.google.samples.apps.nowinandroid.feature.settings.R as SettingsR
/** /**
@ -93,15 +93,15 @@ class NavigationTest {
ReadOnlyProperty<Any?, String> { _, _ -> activity.getString(resId) } ReadOnlyProperty<Any?, String> { _, _ -> activity.getString(resId) }
// The strings used for matching in these tests // The strings used for matching in these tests
private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.navigate_up) private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_navigate_up)
private val forYou by composeTestRule.stringResource(FeatureForyouR.string.for_you) private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_title)
private val interests by composeTestRule.stringResource(FeatureInterestsR.string.interests) private val interests by composeTestRule.stringResource(FeatureSearchR.string.feature_search_interests)
private val sampleTopic = "Headlines" private val sampleTopic = "Headlines"
private val appName by composeTestRule.stringResource(R.string.app_name) private val appName by composeTestRule.stringResource(R.string.app_name)
private val saved by composeTestRule.stringResource(BookmarksR.string.saved) private val saved by composeTestRule.stringResource(BookmarksR.string.feature_bookmarks_title)
private val settings by composeTestRule.stringResource(SettingsR.string.top_app_bar_action_icon_description) private val settings by composeTestRule.stringResource(SettingsR.string.feature_settings_top_app_bar_action_icon_description)
private val brand by composeTestRule.stringResource(SettingsR.string.brand_android) private val brand by composeTestRule.stringResource(SettingsR.string.feature_settings_brand_android)
private val ok by composeTestRule.stringResource(SettingsR.string.dismiss_dialog_button_text) private val ok by composeTestRule.stringResource(SettingsR.string.feature_settings_dismiss_dialog_button_text)
@Before @Before
fun setup() = hiltRule.inject() fun setup() = hiltRule.inject()
@ -166,7 +166,10 @@ class NavigationTest {
composeTestRule.apply { composeTestRule.apply {
// GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown. // GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown.
onNodeWithContentDescription(navigateUp).assertDoesNotExist() onNodeWithContentDescription(navigateUp).assertDoesNotExist()
// TODO: Add top level destinations here, see b/226357686.
onNodeWithText(saved).performClick()
onNodeWithContentDescription(navigateUp).assertDoesNotExist()
onNodeWithText(interests).performClick() onNodeWithText(interests).performClick()
onNodeWithContentDescription(navigateUp).assertDoesNotExist() onNodeWithContentDescription(navigateUp).assertDoesNotExist()
} }

@ -15,6 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<!-- Allow users to distinguish between build variants by having a different background color
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
<color name="ic_launcher_background_tint">#FFFFFF</color> <color name="ic_launcher_background_tint">#FFFFFF</color>
<color name="ic_launcher_foreground_tint">#FF006780</color> <color name="ic_launcher_foreground_tint">#FF006780</color>
</resources> </resources>

@ -15,6 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<!-- Allow users to distinguish between build variants by having a different background color
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
<color name="ic_launcher_background_tint">#000000</color> <color name="ic_launcher_background_tint">#000000</color>
<color name="ic_launcher_foreground_tint">#FF006780</color> <color name="ic_launcher_foreground_tint">#FF006780</color>
</resources> </resources>

@ -15,6 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<!-- Allow users to distinguish between build variants by having a different background color
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
<color name="ic_launcher_background_tint">#FFFFFF</color> <color name="ic_launcher_background_tint">#FFFFFF</color>
<color name="ic_launcher_foreground_tint">#FFA23F16</color> <color name="ic_launcher_foreground_tint">#FFA23F16</color>
</resources> </resources>

@ -15,6 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<!-- Allow users to distinguish between build variants by having a different background color
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
<color name="ic_launcher_background_tint">#000000</color> <color name="ic_launcher_background_tint">#000000</color>
<color name="ic_launcher_foreground_tint">#FFA23F16</color> <color name="ic_launcher_foreground_tint">#FFA23F16</color>
</resources> </resources>

@ -179,7 +179,7 @@ class MainActivity : ComponentActivity() {
To see quick turnaround of the ProfileVerifier, we recommend using `speed-profile`. To see quick turnaround of the ProfileVerifier, we recommend using `speed-profile`.
If you don't do either of these steps, you might only see the profile status reported as If you don't do either of these steps, you might only see the profile status reported as
"enqueued for compilation" when running the sample locally. "enqueued for compilation" when running the sample locally.
*/ */
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val status = ProfileVerifier.getCompilationStatusAsync().await() val status = ProfileVerifier.getCompilationStatusAsync().await()
Log.d(TAG, "ProfileInstaller status code: ${status.profileInstallResultCode}") Log.d(TAG, "ProfileInstaller status code: ${status.profileInstallResultCode}")

@ -20,7 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute 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.foryou.navigation.forYouScreen
import com.google.samples.apps.nowinandroid.feature.interests.navigation.interestsGraph import com.google.samples.apps.nowinandroid.feature.interests.navigation.interestsGraph
import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen
@ -41,7 +41,7 @@ fun NiaNavHost(
appState: NiaAppState, appState: NiaAppState,
onShowSnackbar: suspend (String, String?) -> Boolean, onShowSnackbar: suspend (String, String?) -> Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
startDestination: String = forYouNavigationRoute, startDestination: String = FOR_YOU_ROUTE,
) { ) {
val navController = appState.navController val navController = appState.navController
NavHost( NavHost(

@ -21,7 +21,7 @@ import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR import com.google.samples.apps.nowinandroid.feature.search.R as searchR
/** /**
* Type for the top level destinations in the application. Each of these destinations * Type for the top level destinations in the application. Each of these destinations
@ -37,19 +37,19 @@ enum class TopLevelDestination(
FOR_YOU( FOR_YOU(
selectedIcon = NiaIcons.Upcoming, selectedIcon = NiaIcons.Upcoming,
unselectedIcon = NiaIcons.UpcomingBorder, unselectedIcon = NiaIcons.UpcomingBorder,
iconTextId = forYouR.string.for_you, iconTextId = forYouR.string.feature_foryou_title,
titleTextId = R.string.app_name, titleTextId = R.string.app_name,
), ),
BOOKMARKS( BOOKMARKS(
selectedIcon = NiaIcons.Bookmarks, selectedIcon = NiaIcons.Bookmarks,
unselectedIcon = NiaIcons.BookmarksBorder, unselectedIcon = NiaIcons.BookmarksBorder,
iconTextId = bookmarksR.string.saved, iconTextId = bookmarksR.string.feature_bookmarks_title,
titleTextId = bookmarksR.string.saved, titleTextId = bookmarksR.string.feature_bookmarks_title,
), ),
INTERESTS( INTERESTS(
selectedIcon = NiaIcons.Grid3x3, selectedIcon = NiaIcons.Grid3x3,
unselectedIcon = NiaIcons.Grid3x3, unselectedIcon = NiaIcons.Grid3x3,
iconTextId = interestsR.string.interests, iconTextId = searchR.string.feature_search_interests,
titleTextId = interestsR.string.interests, titleTextId = searchR.string.feature_search_interests,
), ),
} }

@ -183,11 +183,11 @@ fun NiaApp(
titleRes = destination.titleTextId, titleRes = destination.titleTextId,
navigationIcon = NiaIcons.Search, navigationIcon = NiaIcons.Search,
navigationIconContentDescription = stringResource( navigationIconContentDescription = stringResource(
id = settingsR.string.top_app_bar_navigation_icon_description, id = settingsR.string.feature_settings_top_app_bar_navigation_icon_description,
), ),
actionIcon = NiaIcons.Settings, actionIcon = NiaIcons.Settings,
actionIconContentDescription = stringResource( actionIconContentDescription = stringResource(
id = settingsR.string.top_app_bar_action_icon_description, id = settingsR.string.feature_settings_top_app_bar_action_icon_description,
), ),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors( colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent, containerColor = Color.Transparent,

@ -33,11 +33,11 @@ import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BOOKMARKS_ROUTE
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute 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.foryou.navigation.navigateToForYou
import com.google.samples.apps.nowinandroid.feature.interests.navigation.interestsRoute 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.navigateToInterestsGraph
import com.google.samples.apps.nowinandroid.feature.search.navigation.navigateToSearch 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
@ -91,9 +91,9 @@ class NiaAppState(
val currentTopLevelDestination: TopLevelDestination? val currentTopLevelDestination: TopLevelDestination?
@Composable get() = when (currentDestination?.route) { @Composable get() = when (currentDestination?.route) {
forYouNavigationRoute -> FOR_YOU FOR_YOU_ROUTE -> FOR_YOU
bookmarksRoute -> BOOKMARKS BOOKMARKS_ROUTE -> BOOKMARKS
interestsRoute -> INTERESTS INTERESTS_ROUTE -> INTERESTS
else -> null else -> null
} }
@ -115,7 +115,7 @@ class NiaAppState(
* Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the
* route. * route.
*/ */
val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.values().asList() val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.entries
/** /**
* The top level destinations that have unread news resources. * The top level destinations that have unread news resources.

@ -45,7 +45,7 @@ class ScrollTopicListPowerMetricsBenchmark {
@get:Rule @get:Rule
val benchmarkRule = MacrobenchmarkRule() val benchmarkRule = MacrobenchmarkRule()
private val categories = PowerCategory.values() private val categories = PowerCategory.entries
.associateWith { PowerCategoryDisplayLevel.TOTAL } .associateWith { PowerCategoryDisplayLevel.TOTAL }
@Test @Test

@ -60,7 +60,8 @@ class StartupBenchmark {
packageName = PACKAGE_NAME, packageName = PACKAGE_NAME,
metrics = listOf(StartupTimingMetric()), metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode, compilationMode = compilationMode,
iterations = 20, // More iterations result in higher statistical significance. // More iterations result in higher statistical significance.
iterations = 20,
startupMode = COLD, startupMode = COLD,
setupBlock = { setupBlock = {
pressHome() pressHome()

@ -41,6 +41,8 @@ dependencies {
compileOnly(libs.firebase.performance.gradlePlugin) compileOnly(libs.firebase.performance.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin) compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.room.gradlePlugin)
implementation(libs.truth)
} }
tasks { tasks {

@ -24,8 +24,6 @@ class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
pluginManager.apply("com.android.application") pluginManager.apply("com.android.application")
// Screenshot Tests
pluginManager.apply("io.github.takahirom.roborazzi")
val extension = extensions.getByType<ApplicationExtension>() val extension = extensions.getByType<ApplicationExtension>()
configureAndroidCompose(extension) configureAndroidCompose(extension)

@ -21,7 +21,6 @@ import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.kotlin
class AndroidFeatureConventionPlugin : Plugin<Project> { class AndroidFeatureConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
@ -39,27 +38,12 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
} }
dependencies { dependencies {
add("implementation", project(":core:model"))
add("implementation", project(":core:ui")) add("implementation", project(":core:ui"))
add("implementation", project(":core:designsystem")) add("implementation", project(":core:designsystem"))
add("implementation", project(":core:data"))
add("implementation", project(":core:common"))
add("implementation", project(":core:domain"))
add("implementation", project(":core:analytics"))
add("testImplementation", kotlin("test"))
add("testImplementation", project(":core:testing"))
add("androidTestImplementation", kotlin("test"))
add("androidTestImplementation", project(":core:testing"))
add("implementation", libs.findLibrary("coil.kt").get())
add("implementation", libs.findLibrary("coil.kt.compose").get())
add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get()) add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
add("implementation", libs.findLibrary("kotlinx.coroutines.android").get())
} }
} }
} }

@ -30,8 +30,6 @@ class AndroidHiltConventionPlugin : Plugin<Project> {
dependencies { dependencies {
"implementation"(libs.findLibrary("hilt.android").get()) "implementation"(libs.findLibrary("hilt.android").get())
"ksp"(libs.findLibrary("hilt.compiler").get()) "ksp"(libs.findLibrary("hilt.compiler").get())
"kspAndroidTest"(libs.findLibrary("hilt.compiler").get())
"kspTest"(libs.findLibrary("hilt.compiler").get())
} }
} }

@ -26,8 +26,6 @@ class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
pluginManager.apply("com.android.library") pluginManager.apply("com.android.library")
// Screenshot Tests
pluginManager.apply("io.github.takahirom.roborazzi")
val extension = extensions.getByType<LibraryExtension>() val extension = extensions.getByType<LibraryExtension>()
configureAndroidCompose(extension) configureAndroidCompose(extension)

@ -41,6 +41,9 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
defaultConfig.targetSdk = 34 defaultConfig.targetSdk = 34
configureFlavors(this) configureFlavors(this)
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
// The resource prefix is derived from the module name,
// so resources inside ":core:module1" must be prefixed with "core_module1_"
resourcePrefix = path.split("""\W""".toRegex()).drop(1).distinct().joinToString(separator = "_").lowercase() + "_"
} }
extensions.configure<LibraryAndroidComponentsExtension> { extensions.configure<LibraryAndroidComponentsExtension> {
configurePrintApksTask(this) configurePrintApksTask(this)
@ -48,9 +51,6 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
} }
dependencies { dependencies {
add("testImplementation", kotlin("test")) add("testImplementation", kotlin("test"))
add("testImplementation", project(":core:testing"))
add("androidTestImplementation", kotlin("test"))
add("androidTestImplementation", project(":core:testing"))
} }
} }
} }

@ -14,29 +14,25 @@
* limitations under the License. * limitations under the License.
*/ */
import com.google.devtools.ksp.gradle.KspExtension import androidx.room.gradle.RoomExtension
import com.google.samples.apps.nowinandroid.libs import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.process.CommandLineArgumentProvider
import java.io.File
class AndroidRoomConventionPlugin : Plugin<Project> { class AndroidRoomConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
pluginManager.apply("androidx.room")
pluginManager.apply("com.google.devtools.ksp") pluginManager.apply("com.google.devtools.ksp")
extensions.configure<KspExtension> { extensions.configure<RoomExtension> {
// The schemas directory contains a schema file for each version of the Room database. // The schemas directory contains a schema file for each version of the Room database.
// This is required to enable Room auto migrations. // This is required to enable Room auto migrations.
// See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration. // See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration.
arg(RoomSchemaArgProvider(File(projectDir, "schemas"))) schemaDirectory("$projectDir/schemas")
} }
dependencies { dependencies {
@ -46,16 +42,4 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
} }
} }
} }
/**
* https://issuetracker.google.com/issues/132245929
* [Export schemas](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
*/
class RoomSchemaArgProvider(
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
val schemaDir: File,
) : CommandLineArgumentProvider {
override fun asArguments() = listOf("room.schemaLocation=${schemaDir.path}")
}
} }

@ -41,11 +41,6 @@ internal fun Project.configureAndroidCompose(
val bom = libs.findLibrary("androidx-compose-bom").get() val bom = libs.findLibrary("androidx-compose-bom").get()
add("implementation", platform(bom)) add("implementation", platform(bom))
add("androidTestImplementation", platform(bom)) add("androidTestImplementation", platform(bom))
// Add ComponentActivity to debug manifest
add("debugImplementation", libs.findLibrary("androidx.compose.ui.testManifest").get())
// Screenshot Tests on JVM
add("testImplementation", libs.findLibrary("robolectric").get())
add("testImplementation", libs.findLibrary("roborazzi").get())
} }
testOptions { testOptions {

@ -20,8 +20,8 @@ import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.gradle.BaseExtension import com.android.build.gradle.BaseExtension
import com.android.SdkConstants import com.android.SdkConstants
import com.google.common.truth.Truth.assertWithMessage
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty import org.gradle.api.file.RegularFileProperty
@ -40,7 +40,6 @@ import org.gradle.kotlin.dsl.register
import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.gradle.process.ExecOperations import org.gradle.process.ExecOperations
import java.io.File import java.io.File
import java.nio.file.Files
import javax.inject.Inject import javax.inject.Inject
@CacheableTask @CacheableTask
@ -98,17 +97,12 @@ abstract class CheckBadgingTask : DefaultTask() {
@TaskAction @TaskAction
fun taskAction() { fun taskAction() {
if ( assertWithMessage(
Files.mismatch( "Generated badging is different from golden badging! " +
goldenBadging.get().asFile.toPath(), "If this change is intended, run ./gradlew ${updateBadgingTaskName.get()}",
generatedBadging.get().asFile.toPath(), )
) != -1L .that(generatedBadging.get().asFile.readText())
) { .isEqualTo(goldenBadging.get().asFile.readText())
throw GradleException(
"Generated badging is different from golden badging! " +
"If this change is intended, run ./gradlew ${updateBadgingTaskName.get()}",
)
}
} }
} }

@ -83,10 +83,8 @@ private fun Project.configureKotlin() {
val warningsAsErrors: String? by project val warningsAsErrors: String? by project
allWarningsAsErrors = warningsAsErrors.toBoolean() allWarningsAsErrors = warningsAsErrors.toBoolean()
freeCompilerArgs = freeCompilerArgs + listOf( freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.RequiresOptIn",
// Enable experimental coroutines APIs, including Flow // Enable experimental coroutines APIs, including Flow
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
) )
} }
} }

@ -32,6 +32,7 @@ buildscript {
// Lists all plugins used throughout the project without applying them. // Lists all plugins used throughout the project without applying them.
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.test) apply false alias(libs.plugins.android.test) apply false
alias(libs.plugins.baselineprofile) apply false alias(libs.plugins.baselineprofile) apply false
alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.jvm) apply false
@ -44,4 +45,5 @@ plugins {
alias(libs.plugins.ksp) apply false alias(libs.plugins.ksp) apply false
alias(libs.plugins.roborazzi) apply false alias(libs.plugins.roborazzi) apply false
alias(libs.plugins.secrets) apply false alias(libs.plugins.secrets) apply false
alias(libs.plugins.room) apply false
} }

@ -24,9 +24,8 @@ android {
} }
dependencies { dependencies {
implementation(platform(libs.firebase.bom))
implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.core.ktx)
implementation(libs.firebase.analytics) prodImplementation(platform(libs.firebase.bom))
implementation(libs.kotlinx.coroutines.android) prodImplementation(libs.firebase.analytics)
} }

@ -23,7 +23,7 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
abstract class AnalyticsModule { internal abstract class AnalyticsModule {
@Binds @Binds
abstract fun bindsAnalyticsHelper(analyticsHelperImpl: StubAnalyticsHelper): AnalyticsHelper abstract fun bindsAnalyticsHelper(analyticsHelperImpl: StubAnalyticsHelper): AnalyticsHelper
} }

@ -27,7 +27,7 @@ private const val TAG = "StubAnalyticsHelper"
* analytics events should be sent to a backend. * analytics events should be sent to a backend.
*/ */
@Singleton @Singleton
class StubAnalyticsHelper @Inject constructor() : AnalyticsHelper { internal class StubAnalyticsHelper @Inject constructor() : AnalyticsHelper {
override fun logEvent(event: AnalyticsEvent) { override fun logEvent(event: AnalyticsEvent) {
Log.d(TAG, "Received analytics event: $event") Log.d(TAG, "Received analytics event: $event")
} }

@ -28,7 +28,7 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
abstract class AnalyticsModule { internal abstract class AnalyticsModule {
@Binds @Binds
abstract fun bindsAnalyticsHelper(analyticsHelperImpl: FirebaseAnalyticsHelper): AnalyticsHelper abstract fun bindsAnalyticsHelper(analyticsHelperImpl: FirebaseAnalyticsHelper): AnalyticsHelper

@ -23,7 +23,7 @@ import javax.inject.Inject
/** /**
* Implementation of `AnalyticsHelper` which logs events to a Firebase backend. * Implementation of `AnalyticsHelper` which logs events to a Firebase backend.
*/ */
class FirebaseAnalyticsHelper @Inject constructor( internal class FirebaseAnalyticsHelper @Inject constructor(
private val firebaseAnalytics: FirebaseAnalytics, private val firebaseAnalytics: FirebaseAnalytics,
) : AnalyticsHelper { ) : AnalyticsHelper {

@ -24,6 +24,6 @@ android {
} }
dependencies { dependencies {
implementation(libs.kotlinx.coroutines.android) testImplementation(libs.kotlinx.coroutines.test)
testImplementation(projects.core.testing) testImplementation(libs.turbine)
} }

@ -34,7 +34,7 @@ annotation class ApplicationScope
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object CoroutineScopesModule { internal object CoroutineScopesModule {
@Provides @Provides
@Singleton @Singleton
@ApplicationScope @ApplicationScope

@ -24,6 +24,6 @@ android {
dependencies { dependencies {
api(projects.core.data) api(projects.core.data)
implementation(projects.core.testing)
implementation(projects.core.common) implementation(libs.hilt.android.testing)
} }

@ -31,18 +31,16 @@ android {
} }
dependencies { dependencies {
api(projects.core.common)
api(projects.core.database)
api(projects.core.datastore)
api(projects.core.network)
implementation(projects.core.analytics) implementation(projects.core.analytics)
implementation(projects.core.common)
implementation(projects.core.database)
implementation(projects.core.datastore)
implementation(projects.core.model)
implementation(projects.core.network)
implementation(projects.core.notifications) implementation(projects.core.notifications)
implementation(libs.androidx.core.ktx)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.kotlinx.serialization.json)
testImplementation(projects.core.datastoreTest) testImplementation(projects.core.datastoreTest)
testImplementation(projects.core.testing) testImplementation(projects.core.testing)
} }

@ -35,35 +35,35 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface DataModule { abstract class DataModule {
@Binds @Binds
fun bindsTopicRepository( internal abstract fun bindsTopicRepository(
topicsRepository: OfflineFirstTopicsRepository, topicsRepository: OfflineFirstTopicsRepository,
): TopicsRepository ): TopicsRepository
@Binds @Binds
fun bindsNewsResourceRepository( internal abstract fun bindsNewsResourceRepository(
newsRepository: OfflineFirstNewsRepository, newsRepository: OfflineFirstNewsRepository,
): NewsRepository ): NewsRepository
@Binds @Binds
fun bindsUserDataRepository( internal abstract fun bindsUserDataRepository(
userDataRepository: OfflineFirstUserDataRepository, userDataRepository: OfflineFirstUserDataRepository,
): UserDataRepository ): UserDataRepository
@Binds @Binds
fun bindsRecentSearchRepository( internal abstract fun bindsRecentSearchRepository(
recentSearchRepository: DefaultRecentSearchRepository, recentSearchRepository: DefaultRecentSearchRepository,
): RecentSearchRepository ): RecentSearchRepository
@Binds @Binds
fun bindsSearchContentsRepository( internal abstract fun bindsSearchContentsRepository(
searchContentsRepository: DefaultSearchContentsRepository, searchContentsRepository: DefaultSearchContentsRepository,
): SearchContentsRepository ): SearchContentsRepository
@Binds @Binds
fun bindsNetworkMonitor( internal abstract fun bindsNetworkMonitor(
networkMonitor: ConnectivityManagerNetworkMonitor, networkMonitor: ConnectivityManagerNetworkMonitor,
): NetworkMonitor ): NetworkMonitor
} }

@ -25,7 +25,7 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface UserNewsResourceRepositoryModule { internal interface UserNewsResourceRepositoryModule {
@Binds @Binds
fun bindsUserNewsResourceRepository( fun bindsUserNewsResourceRepository(
userDataRepository: CompositeUserNewsResourceRepository, userDataRepository: CompositeUserNewsResourceRepository,

@ -20,7 +20,7 @@ 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.AnalyticsEvent.Param
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
fun AnalyticsHelper.logNewsResourceBookmarkToggled(newsResourceId: String, isBookmarked: Boolean) { internal fun AnalyticsHelper.logNewsResourceBookmarkToggled(newsResourceId: String, isBookmarked: Boolean) {
val eventType = if (isBookmarked) "news_resource_saved" else "news_resource_unsaved" val eventType = if (isBookmarked) "news_resource_saved" else "news_resource_unsaved"
val paramKey = if (isBookmarked) "saved_news_resource_id" else "unsaved_news_resource_id" val paramKey = if (isBookmarked) "saved_news_resource_id" else "unsaved_news_resource_id"
logEvent( logEvent(
@ -33,7 +33,7 @@ fun AnalyticsHelper.logNewsResourceBookmarkToggled(newsResourceId: String, isBoo
) )
} }
fun AnalyticsHelper.logTopicFollowToggled(followedTopicId: String, isFollowed: Boolean) { internal fun AnalyticsHelper.logTopicFollowToggled(followedTopicId: String, isFollowed: Boolean) {
val eventType = if (isFollowed) "topic_followed" else "topic_unfollowed" val eventType = if (isFollowed) "topic_followed" else "topic_unfollowed"
val paramKey = if (isFollowed) "followed_topic_id" else "unfollowed_topic_id" val paramKey = if (isFollowed) "followed_topic_id" else "unfollowed_topic_id"
logEvent( logEvent(
@ -46,7 +46,7 @@ fun AnalyticsHelper.logTopicFollowToggled(followedTopicId: String, isFollowed: B
) )
} }
fun AnalyticsHelper.logThemeChanged(themeName: String) = internal fun AnalyticsHelper.logThemeChanged(themeName: String) =
logEvent( logEvent(
AnalyticsEvent( AnalyticsEvent(
type = "theme_changed", type = "theme_changed",
@ -56,7 +56,7 @@ fun AnalyticsHelper.logThemeChanged(themeName: String) =
), ),
) )
fun AnalyticsHelper.logDarkThemeConfigChanged(darkThemeConfigName: String) = internal fun AnalyticsHelper.logDarkThemeConfigChanged(darkThemeConfigName: String) =
logEvent( logEvent(
AnalyticsEvent( AnalyticsEvent(
type = "dark_theme_config_changed", type = "dark_theme_config_changed",
@ -66,7 +66,7 @@ fun AnalyticsHelper.logDarkThemeConfigChanged(darkThemeConfigName: String) =
), ),
) )
fun AnalyticsHelper.logDynamicColorPreferenceChanged(useDynamicColor: Boolean) = internal fun AnalyticsHelper.logDynamicColorPreferenceChanged(useDynamicColor: Boolean) =
logEvent( logEvent(
AnalyticsEvent( AnalyticsEvent(
type = "dynamic_color_preference_changed", type = "dynamic_color_preference_changed",
@ -76,7 +76,7 @@ fun AnalyticsHelper.logDynamicColorPreferenceChanged(useDynamicColor: Boolean) =
), ),
) )
fun AnalyticsHelper.logOnboardingStateChanged(shouldHideOnboarding: Boolean) { internal fun AnalyticsHelper.logOnboardingStateChanged(shouldHideOnboarding: Boolean) {
val eventType = if (shouldHideOnboarding) "onboarding_complete" else "onboarding_reset" val eventType = if (shouldHideOnboarding) "onboarding_complete" else "onboarding_reset"
logEvent( logEvent(
AnalyticsEvent(type = eventType), AnalyticsEvent(type = eventType),

@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import javax.inject.Inject import javax.inject.Inject
class DefaultRecentSearchRepository @Inject constructor( internal class DefaultRecentSearchRepository @Inject constructor(
private val recentSearchQueryDao: RecentSearchQueryDao, private val recentSearchQueryDao: RecentSearchQueryDao,
) : RecentSearchRepository { ) : RecentSearchRepository {
override suspend fun insertOrReplaceRecentSearch(searchQuery: String) { override suspend fun insertOrReplaceRecentSearch(searchQuery: String) {

@ -36,7 +36,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
class DefaultSearchContentsRepository @Inject constructor( internal class DefaultSearchContentsRepository @Inject constructor(
private val newsResourceDao: NewsResourceDao, private val newsResourceDao: NewsResourceDao,
private val newsResourceFtsDao: NewsResourceFtsDao, private val newsResourceFtsDao: NewsResourceFtsDao,
private val topicDao: TopicDao, private val topicDao: TopicDao,

@ -45,7 +45,7 @@ private const val SYNC_BATCH_SIZE = 40
* Disk storage backed implementation of the [NewsRepository]. * Disk storage backed implementation of the [NewsRepository].
* Reads are exclusively from local storage to support offline access. * Reads are exclusively from local storage to support offline access.
*/ */
class OfflineFirstNewsRepository @Inject constructor( internal class OfflineFirstNewsRepository @Inject constructor(
private val niaPreferencesDataSource: NiaPreferencesDataSource, private val niaPreferencesDataSource: NiaPreferencesDataSource,
private val newsResourceDao: NewsResourceDao, private val newsResourceDao: NewsResourceDao,
private val topicDao: TopicDao, private val topicDao: TopicDao,

@ -34,7 +34,7 @@ import javax.inject.Inject
* Disk storage backed implementation of the [TopicsRepository]. * Disk storage backed implementation of the [TopicsRepository].
* Reads are exclusively from local storage to support offline access. * Reads are exclusively from local storage to support offline access.
*/ */
class OfflineFirstTopicsRepository @Inject constructor( internal class OfflineFirstTopicsRepository @Inject constructor(
private val topicDao: TopicDao, private val topicDao: TopicDao,
private val network: NiaNetworkDataSource, private val network: NiaNetworkDataSource,
) : TopicsRepository { ) : TopicsRepository {

@ -25,7 +25,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
class OfflineFirstUserDataRepository @Inject constructor( internal class OfflineFirstUserDataRepository @Inject constructor(
private val niaPreferencesDataSource: NiaPreferencesDataSource, private val niaPreferencesDataSource: NiaPreferencesDataSource,
private val analyticsHelper: AnalyticsHelper, private val analyticsHelper: AnalyticsHelper,
) : UserDataRepository { ) : UserDataRepository {

@ -33,7 +33,7 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import javax.inject.Inject import javax.inject.Inject
class ConnectivityManagerNetworkMonitor @Inject constructor( internal class ConnectivityManagerNetworkMonitor @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
) : NetworkMonitor { ) : NetworkMonitor {
override val isOnline: Flow<Boolean> = callbackFlow { override val isOnline: Flow<Boolean> = callbackFlow {

@ -30,9 +30,8 @@ android {
} }
dependencies { dependencies {
implementation(projects.core.model) api(projects.core.model)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
androidTestImplementation(projects.core.testing) androidTestImplementation(projects.core.testing)

@ -28,7 +28,7 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object DaosModule { internal object DaosModule {
@Provides @Provides
fun providesTopicsDao( fun providesTopicsDao(
database: NiaDatabase, database: NiaDatabase,

@ -28,7 +28,7 @@ import androidx.room.migration.AutoMigrationSpec
* from and Y is the schema version you're migrating to. The class should implement * from and Y is the schema version you're migrating to. The class should implement
* `AutoMigrationSpec`. * `AutoMigrationSpec`.
*/ */
object DatabaseMigrations { internal object DatabaseMigrations {
@RenameColumn( @RenameColumn(
tableName = "topics", tableName = "topics",

@ -27,7 +27,7 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object DatabaseModule { internal object DatabaseModule {
@Provides @Provides
@Singleton @Singleton
fun providesNiaDatabase( fun providesNiaDatabase(

@ -63,7 +63,7 @@ import com.google.samples.apps.nowinandroid.core.database.util.InstantConverter
@TypeConverters( @TypeConverters(
InstantConverter::class, InstantConverter::class,
) )
abstract class NiaDatabase : RoomDatabase() { internal abstract class NiaDatabase : RoomDatabase() {
abstract fun topicDao(): TopicDao abstract fun topicDao(): TopicDao
abstract fun newsResourceDao(): NewsResourceDao abstract fun newsResourceDao(): NewsResourceDao
abstract fun topicFtsDao(): TopicFtsDao abstract fun topicFtsDao(): TopicFtsDao

@ -19,7 +19,7 @@ package com.google.samples.apps.nowinandroid.core.database.util
import androidx.room.TypeConverter import androidx.room.TypeConverter
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
class InstantConverter { internal class InstantConverter {
@TypeConverter @TypeConverter
fun longToInstant(value: Long?): Instant? = fun longToInstant(value: Long?): Instant? =
value?.let(Instant::fromEpochMilliseconds) value?.let(Instant::fromEpochMilliseconds)

@ -51,5 +51,5 @@ androidComponents.beforeVariants {
} }
dependencies { dependencies {
implementation(libs.protobuf.kotlin.lite) api(libs.protobuf.kotlin.lite)
} }

@ -23,10 +23,7 @@ android {
} }
dependencies { dependencies {
api(projects.core.datastore) implementation(libs.hilt.android.testing)
api(libs.androidx.dataStore.core)
implementation(libs.protobuf.kotlin.lite)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.testing) implementation(projects.core.datastore)
} }

@ -35,7 +35,7 @@ import javax.inject.Singleton
components = [SingletonComponent::class], components = [SingletonComponent::class],
replaces = [DataStoreModule::class], replaces = [DataStoreModule::class],
) )
object TestDataStoreModule { internal object TestDataStoreModule {
@Provides @Provides
@Singleton @Singleton

@ -33,13 +33,12 @@ android {
} }
dependencies { dependencies {
api(libs.androidx.dataStore.core)
api(projects.core.datastoreProto) api(projects.core.datastoreProto)
api(projects.core.model)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.model)
implementation(libs.androidx.dataStore.core)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.protobuf.kotlin.lite)
testImplementation(projects.core.datastoreTest) testImplementation(projects.core.datastoreTest)
testImplementation(projects.core.testing) testImplementation(libs.kotlinx.coroutines.test)
} }

@ -21,7 +21,7 @@ import androidx.datastore.core.DataMigration
/** /**
* Migrates saved ids from [Int] to [String] types * Migrates saved ids from [Int] to [String] types
*/ */
object IntToStringIdsMigration : DataMigration<UserPreferences> { internal object IntToStringIdsMigration : DataMigration<UserPreferences> {
override suspend fun cleanUp() = Unit override suspend fun cleanUp() = Unit

@ -21,7 +21,7 @@ import androidx.datastore.core.DataMigration
/** /**
* Migrates from using lists to maps for user data. * Migrates from using lists to maps for user data.
*/ */
object ListToMapMigration : DataMigration<UserPreferences> { internal object ListToMapMigration : DataMigration<UserPreferences> {
override suspend fun cleanUp() = Unit override suspend fun cleanUp() = Unit

@ -41,7 +41,7 @@ object DataStoreModule {
@Provides @Provides
@Singleton @Singleton
fun providesUserPreferencesDataStore( internal fun providesUserPreferencesDataStore(
@ApplicationContext context: Context, @ApplicationContext context: Context,
@Dispatcher(IO) ioDispatcher: CoroutineDispatcher, @Dispatcher(IO) ioDispatcher: CoroutineDispatcher,
@ApplicationScope scope: CoroutineScope, @ApplicationScope scope: CoroutineScope,

@ -17,6 +17,7 @@ plugins {
alias(libs.plugins.nowinandroid.android.library) alias(libs.plugins.nowinandroid.android.library)
alias(libs.plugins.nowinandroid.android.library.compose) alias(libs.plugins.nowinandroid.android.library.compose)
alias(libs.plugins.nowinandroid.android.library.jacoco) alias(libs.plugins.nowinandroid.android.library.jacoco)
alias(libs.plugins.roborazzi)
} }
android { android {
@ -39,8 +40,15 @@ dependencies {
debugApi(libs.androidx.compose.ui.tooling) debugApi(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.core.ktx)
implementation(libs.coil.kt.compose) implementation(libs.coil.kt.compose)
testImplementation(libs.androidx.compose.ui.test)
testImplementation(libs.accompanist.testharness)
testImplementation(libs.hilt.android.testing)
testImplementation(libs.robolectric)
testImplementation(libs.roborazzi)
testImplementation(projects.core.testing)
androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(projects.core.testing) androidTestImplementation(projects.core.testing)
} }

@ -134,7 +134,7 @@ fun NiaOutlinedButton(
MaterialTheme.colorScheme.outline MaterialTheme.colorScheme.outline
} else { } else {
MaterialTheme.colorScheme.onSurface.copy( MaterialTheme.colorScheme.onSurface.copy(
alpha = NiaButtonDefaults.DisabledOutlinedButtonBorderAlpha, alpha = NiaButtonDefaults.DISABLED_OUTLINED_BUTTON_BORDER_ALPHA,
) )
}, },
), ),
@ -278,7 +278,7 @@ fun NiaButtonPreview() {
@ThemePreviews @ThemePreviews
@Composable @Composable
fun NiaOutlinedButtonPreview() { fun NiaOutlinedButtonPreview() {
NiaTheme() { NiaTheme {
NiaBackground(modifier = Modifier.size(150.dp, 50.dp)) { NiaBackground(modifier = Modifier.size(150.dp, 50.dp)) {
NiaOutlinedButton(onClick = {}, text = { Text("Test button") }) NiaOutlinedButton(onClick = {}, text = { Text("Test button") })
} }
@ -315,7 +315,7 @@ fun NiaButtonLeadingIconPreview() {
object NiaButtonDefaults { object NiaButtonDefaults {
// TODO: File bug // TODO: File bug
// OutlinedButton border color doesn't respect disabled state by default // OutlinedButton border color doesn't respect disabled state by default
const val DisabledOutlinedButtonBorderAlpha = 0.12f const val DISABLED_OUTLINED_BUTTON_BORDER_ALPHA = 0.12f
// TODO: File bug // TODO: File bug
// OutlinedButton default border width isn't exposed via ButtonDefaults // OutlinedButton default border width isn't exposed via ButtonDefaults

@ -76,10 +76,10 @@ fun NiaFilterChip(
borderColor = MaterialTheme.colorScheme.onBackground, borderColor = MaterialTheme.colorScheme.onBackground,
selectedBorderColor = MaterialTheme.colorScheme.onBackground, selectedBorderColor = MaterialTheme.colorScheme.onBackground,
disabledBorderColor = MaterialTheme.colorScheme.onBackground.copy( disabledBorderColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha, alpha = NiaChipDefaults.DISABLED_CHIP_CONTENT_ALPHA,
), ),
disabledSelectedBorderColor = MaterialTheme.colorScheme.onBackground.copy( disabledSelectedBorderColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha, alpha = NiaChipDefaults.DISABLED_CHIP_CONTENT_ALPHA,
), ),
selectedBorderWidth = NiaChipDefaults.ChipBorderWidth, selectedBorderWidth = NiaChipDefaults.ChipBorderWidth,
), ),
@ -88,16 +88,16 @@ fun NiaFilterChip(
iconColor = MaterialTheme.colorScheme.onBackground, iconColor = MaterialTheme.colorScheme.onBackground,
disabledContainerColor = if (selected) { disabledContainerColor = if (selected) {
MaterialTheme.colorScheme.onBackground.copy( MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContainerAlpha, alpha = NiaChipDefaults.DISABLED_CHIP_CONTAINER_ALPHA,
) )
} else { } else {
Color.Transparent Color.Transparent
}, },
disabledLabelColor = MaterialTheme.colorScheme.onBackground.copy( disabledLabelColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha, alpha = NiaChipDefaults.DISABLED_CHIP_CONTENT_ALPHA,
), ),
disabledLeadingIconColor = MaterialTheme.colorScheme.onBackground.copy( disabledLeadingIconColor = MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaChipDefaults.DisabledChipContentAlpha, alpha = NiaChipDefaults.DISABLED_CHIP_CONTENT_ALPHA,
), ),
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedLabelColor = MaterialTheme.colorScheme.onBackground, selectedLabelColor = MaterialTheme.colorScheme.onBackground,
@ -124,7 +124,7 @@ fun ChipPreview() {
object NiaChipDefaults { object NiaChipDefaults {
// TODO: File bug // TODO: File bug
// FilterChip default values aren't exposed via FilterChipDefaults // FilterChip default values aren't exposed via FilterChipDefaults
const val DisabledChipContainerAlpha = 0.12f const val DISABLED_CHIP_CONTAINER_ALPHA = 0.12f
const val DisabledChipContentAlpha = 0.38f const val DISABLED_CHIP_CONTENT_ALPHA = 0.38f
val ChipBorderWidth = 1.dp val ChipBorderWidth = 1.dp
} }

@ -49,7 +49,7 @@ fun DynamicAsyncImage(
imageUrl: String, imageUrl: String,
contentDescription: String?, contentDescription: String?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
placeholder: Painter = painterResource(R.drawable.ic_placeholder_default), placeholder: Painter = painterResource(R.drawable.core_designsystem_ic_placeholder_default),
) { ) {
val iconTint = LocalTintTheme.current.iconTint val iconTint = LocalTintTheme.current.iconTint
var isLoading by remember { mutableStateOf(true) } var isLoading by remember { mutableStateOf(true) }

@ -60,7 +60,7 @@ fun NiaIconToggleButton(
checkedContentColor = MaterialTheme.colorScheme.onPrimaryContainer, checkedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
disabledContainerColor = if (checked) { disabledContainerColor = if (checked) {
MaterialTheme.colorScheme.onBackground.copy( MaterialTheme.colorScheme.onBackground.copy(
alpha = NiaIconButtonDefaults.DisabledIconButtonContainerAlpha, alpha = NiaIconButtonDefaults.DISABLED_ICON_BUTTON_CONTAINER_ALPHA,
) )
} else { } else {
Color.Transparent Color.Transparent
@ -123,5 +123,5 @@ fun IconButtonPreviewUnchecked() {
object NiaIconButtonDefaults { object NiaIconButtonDefaults {
// TODO: File bug // TODO: File bug
// IconToggleButton disabled container alpha not exposed by IconButtonDefaults // IconToggleButton disabled container alpha not exposed by IconButtonDefaults
const val DisabledIconButtonContainerAlpha = 0.12f const val DISABLED_ICON_BUTTON_CONTAINER_ALPHA = 0.12f
} }

@ -40,7 +40,7 @@ fun NiaTopicTag(
MaterialTheme.colorScheme.primaryContainer MaterialTheme.colorScheme.primaryContainer
} else { } else {
MaterialTheme.colorScheme.surfaceVariant.copy( MaterialTheme.colorScheme.surfaceVariant.copy(
alpha = NiaTagDefaults.UnfollowedTopicTagContainerAlpha, alpha = NiaTagDefaults.UNFOLLOWED_TOPIC_TAG_CONTAINER_ALPHA,
) )
} }
TextButton( TextButton(
@ -50,7 +50,7 @@ fun NiaTopicTag(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColorFor(backgroundColor = containerColor), contentColor = contentColorFor(backgroundColor = containerColor),
disabledContainerColor = MaterialTheme.colorScheme.onSurface.copy( disabledContainerColor = MaterialTheme.colorScheme.onSurface.copy(
alpha = NiaTagDefaults.DisabledTopicTagContainerAlpha, alpha = NiaTagDefaults.DISABLED_TOPIC_TAG_CONTAINER_ALPHA,
), ),
), ),
) { ) {
@ -75,9 +75,9 @@ fun TagPreview() {
* Now in Android tag default values. * Now in Android tag default values.
*/ */
object NiaTagDefaults { object NiaTagDefaults {
const val UnfollowedTopicTagContainerAlpha = 0.5f const val UNFOLLOWED_TOPIC_TAG_CONTAINER_ALPHA = 0.5f
// TODO: File bug // TODO: File bug
// Button disabled container alpha value not exposed by ButtonDefaults // Button disabled container alpha value not exposed by ButtonDefaults
const val DisabledTopicTagContainerAlpha = 0.12f const val DISABLED_TOPIC_TAG_CONTAINER_ALPHA = 0.12f
} }

@ -251,5 +251,7 @@ private fun scrollbarThumbColor(
} }
private enum class ThumbState { private enum class ThumbState {
Active, Inactive, Dormant Active,
Inactive,
Dormant,
} }

@ -66,7 +66,7 @@ class LoadingWheelScreenshotTests() {
fun loadingWheelAnimation() { fun loadingWheelAnimation() {
composeTestRule.mainClock.autoAdvance = false composeTestRule.mainClock.autoAdvance = false
composeTestRule.setContent { composeTestRule.setContent {
NiaTheme() { NiaTheme {
NiaLoadingWheel(contentDesc = "") NiaLoadingWheel(contentDesc = "")
} }
} }

@ -24,13 +24,10 @@ android {
} }
dependencies { dependencies {
implementation(projects.core.data) api(projects.core.data)
implementation(projects.core.model) api(projects.core.model)
implementation(libs.hilt.android)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime)
ksp(libs.hilt.compiler) implementation(libs.javax.inject)
testImplementation(projects.core.testing) testImplementation(projects.core.testing)
} }

@ -19,5 +19,5 @@ plugins {
} }
dependencies { dependencies {
implementation(libs.kotlinx.datetime) api(libs.kotlinx.datetime)
} }

@ -17,5 +17,7 @@
package com.google.samples.apps.nowinandroid.core.model.data package com.google.samples.apps.nowinandroid.core.model.data
enum class DarkThemeConfig { enum class DarkThemeConfig {
FOLLOW_SYSTEM, LIGHT, DARK FOLLOW_SYSTEM,
LIGHT,
DARK,
} }

@ -19,7 +19,8 @@ package com.google.samples.apps.nowinandroid.core.model.data
/** /**
* A [topic] with the additional information for whether or not it is followed. * A [topic] with the additional information for whether or not it is followed.
*/ */
data class FollowableTopic( // TODO consider changing to UserTopic and flattening // TODO consider changing to UserTopic and flattening
data class FollowableTopic(
val topic: Topic, val topic: Topic,
val isFollowed: Boolean, val isFollowed: Boolean,
) )

@ -17,5 +17,6 @@
package com.google.samples.apps.nowinandroid.core.model.data package com.google.samples.apps.nowinandroid.core.model.data
enum class ThemeBrand { enum class ThemeBrand {
DEFAULT, ANDROID DEFAULT,
ANDROID,
} }

@ -39,16 +39,16 @@ secrets {
} }
dependencies { dependencies {
implementation(projects.core.common) api(libs.kotlinx.datetime)
implementation(projects.core.model) api(projects.core.common)
api(projects.core.model)
implementation(libs.coil.kt) implementation(libs.coil.kt)
implementation(libs.coil.kt.svg) implementation(libs.coil.kt.svg)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.okhttp.logging) implementation(libs.okhttp.logging)
implementation(libs.retrofit.core) implementation(libs.retrofit.core)
implementation(libs.retrofit.kotlin.serialization) implementation(libs.retrofit.kotlin.serialization)
testImplementation(projects.core.testing) testImplementation(libs.kotlinx.coroutines.test)
} }

@ -25,7 +25,7 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface FlavoredNetworkModule { internal interface FlavoredNetworkModule {
@Binds @Binds
fun binds(impl: FakeNiaNetworkDataSource): NiaNetworkDataSource fun binds(impl: FakeNiaNetworkDataSource): NiaNetworkDataSource

@ -35,7 +35,7 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object NetworkModule { internal object NetworkModule {
@Provides @Provides
@Singleton @Singleton

@ -71,7 +71,7 @@ private data class NetworkResponse<T>(
* [Retrofit] backed [NiaNetworkDataSource] * [Retrofit] backed [NiaNetworkDataSource]
*/ */
@Singleton @Singleton
class RetrofitNiaNetwork @Inject constructor( internal class RetrofitNiaNetwork @Inject constructor(
networkJson: Json, networkJson: Json,
okhttpCallFactory: Call.Factory, okhttpCallFactory: Call.Factory,
) : NiaNetworkDataSource { ) : NiaNetworkDataSource {

@ -25,7 +25,7 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface FlavoredNetworkModule { internal interface FlavoredNetworkModule {
@Binds @Binds
fun binds(impl: RetrofitNiaNetwork): NiaNetworkDataSource fun binds(impl: RetrofitNiaNetwork): NiaNetworkDataSource

@ -44,10 +44,10 @@ class FakeNiaNetworkDataSourceTest {
) )
} }
@Suppress("ktlint:standard:max-line-length")
@Test @Test
fun testDeserializationOfTopics() = runTest(testDispatcher) { fun testDeserializationOfTopics() = runTest(testDispatcher) {
assertEquals( assertEquals(
/* ktlint-disable max-line-length */
NetworkTopic( NetworkTopic(
id = "1", id = "1",
name = "Headlines", name = "Headlines",
@ -56,15 +56,14 @@ class FakeNiaNetworkDataSourceTest {
url = "", url = "",
imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Headlines.svg?alt=media&token=506faab0-617a-4668-9e63-4a2fb996603f", imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Headlines.svg?alt=media&token=506faab0-617a-4668-9e63-4a2fb996603f",
), ),
/* ktlint-enable max-line-length */
subject.getTopics().first(), subject.getTopics().first(),
) )
} }
@Suppress("ktlint:standard:max-line-length")
@Test @Test
fun testDeserializationOfNewsResources() = runTest(testDispatcher) { fun testDeserializationOfNewsResources() = runTest(testDispatcher) {
assertEquals( assertEquals(
/* ktlint-disable max-line-length */
NetworkNewsResource( NetworkNewsResource(
id = "125", id = "125",
title = "Android Basics with Compose", title = "Android Basics with Compose",
@ -83,7 +82,6 @@ class FakeNiaNetworkDataSourceTest {
type = "Codelab", type = "Codelab",
topics = listOf("2", "3", "10"), topics = listOf("2", "3", "10"),
), ),
/* ktlint-enable max-line-length */
subject.getNewsResources().find { it.id == "125" }, subject.getNewsResources().find { it.id == "125" },
) )
} }

@ -15,7 +15,6 @@
*/ */
plugins { plugins {
alias(libs.plugins.nowinandroid.android.library) alias(libs.plugins.nowinandroid.android.library)
alias(libs.plugins.nowinandroid.android.library.compose)
alias(libs.plugins.nowinandroid.android.hilt) alias(libs.plugins.nowinandroid.android.hilt)
} }
@ -24,14 +23,10 @@ android {
} }
dependencies { dependencies {
implementation(projects.core.common) api(projects.core.model)
implementation(projects.core.model)
implementation(libs.kotlinx.coroutines.android) implementation(projects.core.common)
implementation(libs.androidx.browser)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.core.ktx)
implementation(platform(libs.firebase.bom)) compileOnly(platform(libs.androidx.compose.bom))
implementation(libs.firebase.cloud.messaging) compileOnly(libs.androidx.compose.runtime)
} }

@ -23,7 +23,7 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
abstract class NotificationsModule { internal abstract class NotificationsModule {
@Binds @Binds
abstract fun bindNotifier( abstract fun bindNotifier(
notifier: NoOpNotifier, notifier: NoOpNotifier,

@ -22,6 +22,6 @@ import javax.inject.Inject
/** /**
* Implementation of [Notifier] which does nothing. Useful for tests and previews. * Implementation of [Notifier] which does nothing. Useful for tests and previews.
*/ */
class NoOpNotifier @Inject constructor() : Notifier { internal class NoOpNotifier @Inject constructor() : Notifier {
override fun postNewsNotifications(newsResources: List<NewsResource>) = Unit override fun postNewsNotifications(newsResources: List<NewsResource>) = Unit
} }

@ -50,7 +50,7 @@ private const val FOR_YOU_PATH = "foryou"
* Implementation of [Notifier] that displays notifications in the system tray. * Implementation of [Notifier] that displays notifications in the system tray.
*/ */
@Singleton @Singleton
class SystemTrayNotifier @Inject constructor( internal class SystemTrayNotifier @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
) : Notifier { ) : Notifier {
@ -72,7 +72,7 @@ class SystemTrayNotifier @Inject constructor(
.map { newsResource -> .map { newsResource ->
createNewsNotification { createNewsNotification {
setSmallIcon( setSmallIcon(
com.google.samples.apps.nowinandroid.core.common.R.drawable.ic_nia_notification, com.google.samples.apps.nowinandroid.core.common.R.drawable.core_common_ic_nia_notification,
) )
.setContentTitle(newsResource.title) .setContentTitle(newsResource.title)
.setContentText(newsResource.content) .setContentText(newsResource.content)
@ -83,13 +83,13 @@ class SystemTrayNotifier @Inject constructor(
} }
val summaryNotification = createNewsNotification { val summaryNotification = createNewsNotification {
val title = getString( val title = getString(
R.string.news_notification_group_summary, R.string.core_notifications_news_notification_group_summary,
truncatedNewsResources.size, truncatedNewsResources.size,
) )
setContentTitle(title) setContentTitle(title)
.setContentText(title) .setContentText(title)
.setSmallIcon( .setSmallIcon(
com.google.samples.apps.nowinandroid.core.common.R.drawable.ic_nia_notification, com.google.samples.apps.nowinandroid.core.common.R.drawable.core_common_ic_nia_notification,
) )
// Build summary info into InboxStyle template. // Build summary info into InboxStyle template.
.setStyle(newsNotificationStyle(truncatedNewsResources, title)) .setStyle(newsNotificationStyle(truncatedNewsResources, title))
@ -148,10 +148,10 @@ private fun Context.ensureNotificationChannelExists() {
val channel = NotificationChannel( val channel = NotificationChannel(
NEWS_NOTIFICATION_CHANNEL_ID, NEWS_NOTIFICATION_CHANNEL_ID,
getString(R.string.news_notification_channel_name), getString(R.string.core_notifications_news_notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT, NotificationManager.IMPORTANCE_DEFAULT,
).apply { ).apply {
description = getString(R.string.news_notification_channel_description) description = getString(R.string.core_notifications_news_notification_channel_description)
} }
// Register the channel with the system // Register the channel with the system
NotificationManagerCompat.from(this).createNotificationChannel(channel) NotificationManagerCompat.from(this).createNotificationChannel(channel)

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<string name="news_notification_channel_name">News updates</string> <string name="core_notifications_news_notification_channel_name">News updates</string>
<string name="news_notification_channel_description">The latest updates on what\'s new in Android</string> <string name="core_notifications_news_notification_channel_description">The latest updates on what\'s new in Android</string>
<string name="news_notification_group_summary">%1$d news updates</string> <string name="core_notifications_news_notification_group_summary">%1$d news updates</string>
</resources> </resources>

@ -23,7 +23,7 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
abstract class NotificationsModule { internal abstract class NotificationsModule {
@Binds @Binds
abstract fun bindNotifier( abstract fun bindNotifier(
notifier: SystemTrayNotifier, notifier: SystemTrayNotifier,

@ -24,28 +24,23 @@ android {
} }
dependencies { dependencies {
api(libs.accompanist.testharness) api(kotlin("test"))
api(libs.androidx.activity.compose)
api(libs.androidx.compose.ui.test) api(libs.androidx.compose.ui.test)
api(libs.androidx.test.core)
api(libs.androidx.test.espresso.core)
api(libs.androidx.test.rules)
api(libs.androidx.test.runner)
api(libs.hilt.android.testing)
api(libs.junit4)
api(libs.kotlinx.coroutines.test)
api(libs.roborazzi) api(libs.roborazzi)
api(libs.robolectric.shadows) api(projects.core.analytics)
api(libs.turbine) api(projects.core.data)
api(projects.core.model)
api(projects.core.notifications)
debugApi(libs.androidx.compose.ui.testManifest) debugApi(libs.androidx.compose.ui.testManifest)
implementation(libs.accompanist.testharness)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.test.rules)
implementation(libs.hilt.android.testing)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotlinx.datetime)
implementation(libs.robolectric.shadows)
implementation(projects.core.common) implementation(projects.core.common)
implementation(projects.core.data)
implementation(projects.core.designsystem) implementation(projects.core.designsystem)
implementation(projects.core.domain)
implementation(projects.core.model)
implementation(projects.core.notifications)
implementation(projects.core.analytics)
implementation(libs.kotlinx.datetime)
} }

@ -14,12 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
@file:Suppress("ktlint:standard:max-line-length")
package com.google.samples.apps.nowinandroid.core.testing.data package com.google.samples.apps.nowinandroid.core.testing.data
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
/* ktlint-disable max-line-length */
val followableTopicTestData: List<FollowableTopic> = listOf( val followableTopicTestData: List<FollowableTopic> = listOf(
FollowableTopic( FollowableTopic(
topic = Topic( topic = Topic(

@ -14,12 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
@file:Suppress("ktlint:standard:max-line-length")
package com.google.samples.apps.nowinandroid.core.testing.data package com.google.samples.apps.nowinandroid.core.testing.data
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
/* ktlint-disable max-line-length */
val newsResourcesTestData: List<NewsResource> = listOf( val newsResourcesTestData: List<NewsResource> = listOf(
NewsResource( NewsResource(
id = "1", id = "1",

@ -14,11 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
@file:Suppress("ktlint:standard:max-line-length")
package com.google.samples.apps.nowinandroid.core.testing.data package com.google.samples.apps.nowinandroid.core.testing.data
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
/* ktlint-disable max-line-length */
val topicsTestData: List<Topic> = listOf( val topicsTestData: List<Topic> = listOf(
Topic( Topic(
id = "2", id = "2",

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
@file:Suppress("ktlint:standard:max-line-length")
package com.google.samples.apps.nowinandroid.core.testing.data package com.google.samples.apps.nowinandroid.core.testing.data
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
@ -26,7 +28,6 @@ import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
/* ktlint-disable max-line-length */
val userNewsResourcesTestData: List<UserNewsResource> = UserData( val userNewsResourcesTestData: List<UserNewsResource> = UserData(
bookmarkedNewsResources = setOf("1", "4"), bookmarkedNewsResources = setOf("1", "4"),
viewedNewsResources = setOf("1", "2", "4"), viewedNewsResources = setOf("1", "2", "4"),

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save