Merge branch 'android-remote-main' into fix-snackbar

Change-Id: If0f12500cb55daae98fd7ee0951d3d3384d26b0e

# Conflicts:
#	app/src/testDemo/screenshots/insets_snackbar_compact_medium.png
#	app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png
#	app/src/testDemo/screenshots/insets_snackbar_medium_medium.png
pull/1837/head
Jaehwa Noh 7 months ago
commit 8ac8dc31f6

@ -5,3 +5,13 @@
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 ktlint_function_naming_ignore_when_annotated_with=Composable, Test
ktlint_standard_backing-property-naming = disabled
ktlint_standard_binary-expression-wrapping = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_condition-wrapping = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-literal = disabled
ktlint_standard_function-type-modifier-spacing = disabled
ktlint_standard_multiline-loop = disabled
ktlint_standard_function-signature = disabled

@ -17,6 +17,8 @@
org.gradle.daemon=false org.gradle.daemon=false
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.workers.max=2 org.gradle.workers.max=2
org.gradle.configuration-cache=true
org.gradle.configuration-cache.parallel=true
kotlin.incremental=false kotlin.incremental=false

@ -1,25 +0,0 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "weekly"
registries: "*"
labels: [ "version update" ]
groups:
kotlin-ksp:
patterns:
- "org.jetbrains.kotlin:*"
- "org.jetbrains.kotlin.jvm"
- "com.google.devtools.ksp"
open-pull-requests-limit: 10
registries:
maven-google:
type: "maven-repository"
url: "https://maven.google.com"
replaces-base: true

@ -0,0 +1,14 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>android/.github:renovate-config"
],
"baseBranches": [
"main"
],
"gitIgnoredAuthors": [
"renovate[bot]@users.noreply.github.com",
"github-actions[bot]@users.noreply.github.com",
"41898282+github-actions[bot]@users.noreply.github.com"
]
}

@ -18,6 +18,7 @@ jobs:
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
security-events: write
timeout-minutes: 60 timeout-minutes: 60
@ -25,13 +26,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls /dev/kvm
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
@ -43,15 +37,11 @@ jobs:
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with:
- name: Setup Android SDK cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
uses: android-actions/setup-android@v3
- name: Accept licenses
run: yes | sdkmanager --licenses || true
- name: Check build-logic - name: Check build-logic
run: ./gradlew check -p build-logic run: ./gradlew :build-logic:convention:check
- name: Check spotless - name: Check spotless
run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache
@ -93,7 +83,9 @@ jobs:
continue-on-error: false continue-on-error: false
if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
run: | run: |
echo "::error::Screenshot tests failed, please create a PR in your fork first." && exit 1 echo "::error::Screenshot tests failed, please create a PR in your fork first."
echo "Your fork's CI will take screenshots for your fork."
exit 1
# Runs if previous job failed # Runs if previous job failed
- name: Generate new screenshots if verification failed and it's a PR - name: Generate new screenshots if verification failed and it's a PR
@ -114,19 +106,8 @@ jobs:
- name: Run local tests - name: Run local tests
run: ./gradlew testDemoDebug :lint:test run: ./gradlew testDemoDebug :lint:test
- name: Setup GMD
run: ./gradlew :benchmarks:pixel6Api33Setup
--info
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
- name: Build all build type and flavor permutations - name: Build all build type and flavor permutations
run: ./gradlew :app:assemble :benchmarks:assemble -Pandroidx.baselineprofile.skipgeneration run: ./gradlew :app:assemble
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
-Pandroid.experimental.androidTest.numManagedDeviceShards=1
-Pandroid.experimental.testOptions.managedDevices.maxConcurrentDevices=1
-Pandroid.experimental.testOptions.managedDevices.setupTimeoutMinutes=5
- name: Upload build outputs (APKs) - name: Upload build outputs (APKs)
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@ -135,14 +116,14 @@ jobs:
path: '**/build/outputs/apk/**/*.apk' path: '**/build/outputs/apk/**/*.apk'
- name: Upload JVM local results (XML) - name: Upload JVM local results (XML)
if: always() if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: local-test-results name: local-test-results
path: '**/build/test-results/test*UnitTest/**.xml' path: '**/build/test-results/test*UnitTest/**.xml'
- name: Upload screenshot results (PNG) - name: Upload screenshot results (PNG)
if: always() if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: screenshot-test-results name: screenshot-test-results
@ -152,12 +133,18 @@ jobs:
run: ./gradlew :app:lintProdRelease :app-nia-catalog:lintRelease :lint:lint run: ./gradlew :app:lintProdRelease :app-nia-catalog:lintRelease :lint:lint
- name: Upload lint reports (HTML) - name: Upload lint reports (HTML)
if: always() if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4 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'
- name: Upload lint reports (SARIF)
if: ${{ !cancelled() && hashFiles('**/*.sarif') != '' }}
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: './'
- name: Check badging - name: Check badging
run: ./gradlew :app:checkProdReleaseBadging run: ./gradlew :app:checkProdReleaseBadging
@ -166,7 +153,7 @@ jobs:
timeout-minutes: 55 timeout-minutes: 55
strategy: strategy:
matrix: matrix:
api-level: [26, 30] api-level: [26, 34]
steps: steps:
- name: Delete unnecessary tools 🔧 - name: Delete unnecessary tools 🔧
@ -202,8 +189,7 @@ jobs:
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with: with:
validate-wrappers: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
gradle-home-cache-cleanup: true
- name: Build projects and run instrumentation tests - name: Build projects and run instrumentation tests
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
@ -226,7 +212,7 @@ jobs:
run: ./gradlew createDemoDebugCombinedCoverageReport run: ./gradlew createDemoDebugCombinedCoverageReport
- name: Upload test reports - name: Upload test reports
if: always() if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: test-reports-${{ matrix.api-level }} name: test-reports-${{ matrix.api-level }}
@ -235,7 +221,7 @@ jobs:
- name: Display local test coverage (only API 30) - name: Display local test coverage (only API 30)
if: matrix.api-level == 30 if: matrix.api-level == 30
id: jacoco id: jacoco
uses: madrapps/jacoco-report@v1.7.0 uses: madrapps/jacoco-report@v1.7.1
with: with:
title: Combined test coverage report title: Combined test coverage report
min-coverage-overall: 40 min-coverage-overall: 40

@ -7,6 +7,7 @@ on:
jobs: jobs:
baseline_profiles: baseline_profiles:
name: "Generate Baseline Profiles" name: "Generate Baseline Profiles"
if: github.repository == 'android/nowinandroid'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@ -36,6 +37,8 @@ jobs:
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
@ -44,7 +47,7 @@ jobs:
run: yes | sdkmanager --licenses || true run: yes | sdkmanager --licenses || true
- name: Check build-logic - name: Check build-logic
run: ./gradlew check -p build-logic run: ./gradlew :build-logic:convention:check
- name: Setup GMD - name: Setup GMD
run: ./gradlew :benchmarks:pixel6Api33Setup run: ./gradlew :benchmarks:pixel6Api33Setup

@ -7,6 +7,7 @@ on:
jobs: jobs:
build: build:
if: github.repository == 'android/nowinandroid'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 120 timeout-minutes: 120
@ -33,6 +34,8 @@ jobs:
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3

@ -115,8 +115,9 @@ To run the tests execute the following gradle tasks:
(see below for explanation). To avoid this, run `recordRoborazziDemoDebug` prior to running unit tests. (see below for explanation). To avoid this, run `recordRoborazziDemoDebug` prior to running unit tests.
- `connectedDemoDebugAndroidTest` run all instrumented tests against the `demoDebug` variant. - `connectedDemoDebugAndroidTest` run all instrumented tests against the `demoDebug` variant.
**Note:** You should not run `./gradlew test` or `./gradlew connectedAndroidTest` as this will execute > [!NOTE]
tests against _all_ build variants which is both unecessary and will result in failures as only the > You should not run `./gradlew test` or `./gradlew connectedAndroidTest` as this will execute
tests against _all_ build variants which is both unnecessary and will result in failures as only the
`demoDebug` variant is supported. No other variants have any tests (although this might change in future). `demoDebug` variant is supported. No other variants have any tests (although this might change in future).
## Screenshot tests ## Screenshot tests
@ -138,7 +139,9 @@ stored in `modulename/src/test/screenshots`.
- `compareRoborazziDemoDebug` create comparison images between failed tests and the known correct - `compareRoborazziDemoDebug` create comparison images between failed tests and the known correct
images. These can also be found in `modulename/src/test/screenshots`. images. These can also be found in `modulename/src/test/screenshots`.
**Note on failing screenshot tests:** The known correct screenshots stored in this repository are recorded on CI using Linux. Other > [!NOTE]
> **Note on failing screenshot tests**
> The known correct screenshots stored in this repository are recorded on CI using Linux. Other
platforms may (and probably will) generate slightly different images, making the screenshot tests fail. 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 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 `main` branch before starting work. After making changes, `verifyRoborazziDemoDebug` will identify only

@ -2,85 +2,85 @@ androidx.activity:activity-compose:1.9.3
androidx.activity:activity-ktx:1.9.3 androidx.activity:activity-ktx:1.9.3
androidx.activity:activity:1.9.3 androidx.activity:activity:1.9.3
androidx.annotation:annotation-experimental:1.4.1 androidx.annotation:annotation-experimental:1.4.1
androidx.annotation:annotation-jvm:1.8.1 androidx.annotation:annotation-jvm:1.9.1
androidx.annotation:annotation:1.8.1 androidx.annotation:annotation:1.9.1
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.8.0 androidx.browser:browser:1.8.0
androidx.collection:collection-jvm:1.4.4 androidx.collection:collection-jvm:1.5.0-beta01
androidx.collection:collection-ktx:1.4.4 androidx.collection:collection-ktx:1.5.0-beta01
androidx.collection:collection:1.4.4 androidx.collection:collection:1.5.0-beta01
androidx.compose.animation:animation-android:1.7.5 androidx.compose.animation:animation-android:1.8.0-alpha07
androidx.compose.animation:animation-core-android:1.7.5 androidx.compose.animation:animation-core-android:1.8.0-alpha07
androidx.compose.animation:animation-core:1.7.5 androidx.compose.animation:animation-core:1.8.0-alpha07
androidx.compose.animation:animation:1.7.5 androidx.compose.animation:animation:1.8.0-alpha07
androidx.compose.foundation:foundation-android:1.7.5 androidx.compose.foundation:foundation-android:1.8.0-alpha07
androidx.compose.foundation:foundation-layout-android:1.7.5 androidx.compose.foundation:foundation-layout-android:1.8.0-alpha07
androidx.compose.foundation:foundation-layout:1.7.5 androidx.compose.foundation:foundation-layout:1.8.0-alpha07
androidx.compose.foundation:foundation:1.7.5 androidx.compose.foundation:foundation:1.8.0-alpha07
androidx.compose.material3.adaptive:adaptive-android:1.0.0 androidx.compose.material3.adaptive:adaptive-android:1.0.0
androidx.compose.material3.adaptive:adaptive:1.0.0 androidx.compose.material3.adaptive:adaptive:1.0.0
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1 androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1
androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1 androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1
androidx.compose.material3:material3-android:1.3.1 androidx.compose.material3:material3-android:1.3.1
androidx.compose.material3:material3:1.3.1 androidx.compose.material3:material3:1.3.1
androidx.compose.material:material-icons-core-android:1.7.5 androidx.compose.material:material-icons-core-android:1.7.6
androidx.compose.material:material-icons-core:1.7.5 androidx.compose.material:material-icons-core:1.7.6
androidx.compose.material:material-icons-extended-android:1.7.5 androidx.compose.material:material-icons-extended-android:1.7.6
androidx.compose.material:material-icons-extended:1.7.5 androidx.compose.material:material-icons-extended:1.7.6
androidx.compose.material:material-ripple-android:1.7.5 androidx.compose.material:material-ripple-android:1.7.6
androidx.compose.material:material-ripple:1.7.5 androidx.compose.material:material-ripple:1.7.6
androidx.compose.runtime:runtime-android:1.7.5 androidx.compose.runtime:runtime-android:1.8.0-alpha07
androidx.compose.runtime:runtime-saveable-android:1.7.5 androidx.compose.runtime:runtime-saveable-android:1.8.0-alpha07
androidx.compose.runtime:runtime-saveable:1.7.5 androidx.compose.runtime:runtime-saveable:1.8.0-alpha07
androidx.compose.runtime:runtime:1.7.5 androidx.compose.runtime:runtime:1.8.0-alpha07
androidx.compose.ui:ui-android:1.7.5 androidx.compose.ui:ui-android:1.8.0-alpha07
androidx.compose.ui:ui-geometry-android:1.7.5 androidx.compose.ui:ui-geometry-android:1.8.0-alpha07
androidx.compose.ui:ui-geometry:1.7.5 androidx.compose.ui:ui-geometry:1.8.0-alpha07
androidx.compose.ui:ui-graphics-android:1.7.5 androidx.compose.ui:ui-graphics-android:1.8.0-alpha07
androidx.compose.ui:ui-graphics:1.7.5 androidx.compose.ui:ui-graphics:1.8.0-alpha07
androidx.compose.ui:ui-text-android:1.7.5 androidx.compose.ui:ui-text-android:1.8.0-alpha07
androidx.compose.ui:ui-text:1.7.5 androidx.compose.ui:ui-text:1.8.0-alpha07
androidx.compose.ui:ui-tooling-preview-android:1.7.5 androidx.compose.ui:ui-tooling-preview-android:1.8.0-alpha07
androidx.compose.ui:ui-tooling-preview:1.7.5 androidx.compose.ui:ui-tooling-preview:1.8.0-alpha07
androidx.compose.ui:ui-unit-android:1.7.5 androidx.compose.ui:ui-unit-android:1.8.0-alpha07
androidx.compose.ui:ui-unit:1.7.5 androidx.compose.ui:ui-unit:1.8.0-alpha07
androidx.compose.ui:ui-util-android:1.7.5 androidx.compose.ui:ui-util-android:1.8.0-alpha07
androidx.compose.ui:ui-util:1.7.5 androidx.compose.ui:ui-util:1.8.0-alpha07
androidx.compose.ui:ui:1.7.5 androidx.compose.ui:ui:1.8.0-alpha07
androidx.compose:compose-bom:2024.11.00 androidx.compose:compose-bom:2024.12.01
androidx.concurrent:concurrent-futures:1.1.0 androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.13.1 androidx.core:core-ktx:1.13.1
androidx.core:core:1.13.1 androidx.core:core:1.13.1
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.emoji2:emoji2:1.3.0 androidx.emoji2:emoji2:1.4.0
androidx.exifinterface:exifinterface:1.3.7 androidx.exifinterface:exifinterface:1.3.7
androidx.fragment:fragment:1.5.1 androidx.fragment:fragment:1.5.1
androidx.graphics:graphics-path:1.0.1 androidx.graphics:graphics-path:1.0.1
androidx.interpolator:interpolator:1.0.0 androidx.interpolator:interpolator:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.8.3 androidx.lifecycle:lifecycle-common-java8:2.8.7
androidx.lifecycle:lifecycle-common-jvm:2.8.3 androidx.lifecycle:lifecycle-common-jvm:2.8.7
androidx.lifecycle:lifecycle-common:2.8.3 androidx.lifecycle:lifecycle-common:2.8.7
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.3 androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.7
androidx.lifecycle:lifecycle-livedata-core:2.8.3 androidx.lifecycle:lifecycle-livedata-core:2.8.7
androidx.lifecycle:lifecycle-livedata:2.8.3 androidx.lifecycle:lifecycle-livedata:2.8.7
androidx.lifecycle:lifecycle-process:2.8.3 androidx.lifecycle:lifecycle-process:2.8.7
androidx.lifecycle:lifecycle-runtime-android:2.8.3 androidx.lifecycle:lifecycle-runtime-android:2.8.7
androidx.lifecycle:lifecycle-runtime-compose-android:2.8.3 androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7
androidx.lifecycle:lifecycle-runtime-compose:2.8.3 androidx.lifecycle:lifecycle-runtime-compose:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.3 androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx:2.8.3 androidx.lifecycle:lifecycle-runtime-ktx:2.8.7
androidx.lifecycle:lifecycle-runtime:2.8.3 androidx.lifecycle:lifecycle-runtime:2.8.7
androidx.lifecycle:lifecycle-viewmodel-android:2.8.3 androidx.lifecycle:lifecycle-viewmodel-android:2.8.7
androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3 androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.3 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7
androidx.lifecycle:lifecycle-viewmodel:2.8.3 androidx.lifecycle:lifecycle-viewmodel:2.8.7
androidx.loader:loader:1.0.0 androidx.loader:loader:1.0.0
androidx.metrics:metrics-performance:1.0.0-beta01 androidx.metrics:metrics-performance:1.0.0-beta01
androidx.profileinstaller:profileinstaller:1.3.1 androidx.profileinstaller:profileinstaller:1.4.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.startup:startup-runtime:1.1.1 androidx.startup:startup-runtime:1.1.1
@ -96,10 +96,10 @@ androidx.window:window-core:1.3.0
androidx.window:window:1.3.0 androidx.window:window:1.3.0
com.google.accompanist:accompanist-drawablepainter:0.32.0 com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.code.findbugs:jsr305:3.0.2 com.google.code.findbugs:jsr305:3.0.2
com.google.dagger:dagger-lint-aar:2.52 com.google.dagger:dagger-lint-aar:2.54
com.google.dagger:dagger:2.52 com.google.dagger:dagger:2.54
com.google.dagger:hilt-android:2.52 com.google.dagger:hilt-android:2.54
com.google.dagger:hilt-core:2.52 com.google.dagger:hilt-core:2.54
com.google.guava:listenablefuture:1.0 com.google.guava:listenablefuture:1.0
com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0 com.squareup.okio:okio-jvm:3.9.0
@ -110,10 +110,10 @@ io.coil-kt:coil-compose:2.7.0
io.coil-kt:coil:2.7.0 io.coil-kt:coil:2.7.0
jakarta.inject:jakarta.inject-api:2.0.1 jakarta.inject:jakarta.inject-api:2.0.1
javax.inject:javax.inject:1 javax.inject:javax.inject:1
org.jetbrains.kotlin:kotlin-stdlib-common:2.0.20 org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
org.jetbrains.kotlin:kotlin-stdlib:2.0.20 org.jetbrains.kotlin:kotlin-stdlib:2.1.0
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1
@ -121,3 +121,4 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1 org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1
org.jetbrains.kotlinx:kotlinx-datetime:0.6.1 org.jetbrains.kotlinx:kotlinx-datetime:0.6.1
org.jetbrains:annotations:23.0.0 org.jetbrains:annotations:23.0.0
org.jspecify:jspecify:1.0.0

@ -36,9 +36,6 @@ android {
// Custom test runner to set up Hilt dependency graph // Custom test runner to set up Hilt dependency graph
testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
vectorDrawables {
useSupportLibrary = true
}
} }
buildTypes { buildTypes {
@ -119,18 +116,19 @@ dependencies {
testImplementation(projects.sync.syncTest) testImplementation(projects.sync.syncTest)
testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test)
testDemoImplementation(libs.androidx.navigation.testing)
testDemoImplementation(libs.robolectric) testDemoImplementation(libs.robolectric)
testDemoImplementation(libs.roborazzi) testDemoImplementation(libs.roborazzi)
testDemoImplementation(projects.core.screenshotTesting) testDemoImplementation(projects.core.screenshotTesting)
testDemoImplementation(projects.core.testing)
androidTestImplementation(kotlin("test"))
androidTestImplementation(projects.core.testing) androidTestImplementation(projects.core.testing)
androidTestImplementation(projects.core.dataTest) androidTestImplementation(projects.core.dataTest)
androidTestImplementation(projects.core.datastoreTest) androidTestImplementation(projects.core.datastoreTest)
androidTestImplementation(libs.androidx.test.espresso.core) androidTestImplementation(libs.androidx.test.espresso.core)
androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.androidx.compose.ui.test) androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(libs.hilt.android.testing) androidTestImplementation(libs.hilt.android.testing)
androidTestImplementation(libs.kotlin.test)
baselineProfile(projects.benchmarks) baselineProfile(projects.benchmarks)
} }

@ -2,25 +2,25 @@ androidx.activity:activity-compose:1.9.3
androidx.activity:activity-ktx:1.9.3 androidx.activity:activity-ktx:1.9.3
androidx.activity:activity:1.9.3 androidx.activity:activity:1.9.3
androidx.annotation:annotation-experimental:1.4.1 androidx.annotation:annotation-experimental:1.4.1
androidx.annotation:annotation-jvm:1.8.1 androidx.annotation:annotation-jvm:1.9.1
androidx.annotation:annotation:1.8.1 androidx.annotation:annotation:1.9.1
androidx.appcompat:appcompat-resources:1.7.0 androidx.appcompat:appcompat-resources:1.7.0
androidx.appcompat:appcompat:1.7.0 androidx.appcompat:appcompat:1.7.0
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.8.0 androidx.browser:browser:1.8.0
androidx.collection:collection-jvm:1.4.4 androidx.collection:collection-jvm:1.5.0-beta01
androidx.collection:collection-ktx:1.4.4 androidx.collection:collection-ktx:1.5.0-beta01
androidx.collection:collection:1.4.4 androidx.collection:collection:1.5.0-beta01
androidx.compose.animation:animation-android:1.7.5 androidx.compose.animation:animation-android:1.8.0-alpha07
androidx.compose.animation:animation-core-android:1.7.5 androidx.compose.animation:animation-core-android:1.8.0-alpha07
androidx.compose.animation:animation-core:1.7.5 androidx.compose.animation:animation-core:1.8.0-alpha07
androidx.compose.animation:animation:1.7.5 androidx.compose.animation:animation:1.8.0-alpha07
androidx.compose.foundation:foundation-android:1.7.5 androidx.compose.foundation:foundation-android:1.8.0-alpha07
androidx.compose.foundation:foundation-layout-android:1.7.5 androidx.compose.foundation:foundation-layout-android:1.8.0-alpha07
androidx.compose.foundation:foundation-layout:1.7.5 androidx.compose.foundation:foundation-layout:1.8.0-alpha07
androidx.compose.foundation:foundation:1.7.5 androidx.compose.foundation:foundation:1.8.0-alpha07
androidx.compose.material3.adaptive:adaptive-android:1.0.0 androidx.compose.material3.adaptive:adaptive-android:1.0.0
androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0 androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0
androidx.compose.material3.adaptive:adaptive-layout:1.0.0 androidx.compose.material3.adaptive:adaptive-layout:1.0.0
@ -33,36 +33,37 @@ androidx.compose.material3:material3-android:1.3.1
androidx.compose.material3:material3-window-size-class-android:1.3.1 androidx.compose.material3:material3-window-size-class-android:1.3.1
androidx.compose.material3:material3-window-size-class:1.3.1 androidx.compose.material3:material3-window-size-class:1.3.1
androidx.compose.material3:material3:1.3.1 androidx.compose.material3:material3:1.3.1
androidx.compose.material:material-icons-core-android:1.7.5 androidx.compose.material:material-icons-core-android:1.7.6
androidx.compose.material:material-icons-core:1.7.5 androidx.compose.material:material-icons-core:1.7.6
androidx.compose.material:material-icons-extended-android:1.7.5 androidx.compose.material:material-icons-extended-android:1.7.6
androidx.compose.material:material-icons-extended:1.7.5 androidx.compose.material:material-icons-extended:1.7.6
androidx.compose.material:material-ripple-android:1.7.5 androidx.compose.material:material-ripple-android:1.7.6
androidx.compose.material:material-ripple:1.7.5 androidx.compose.material:material-ripple:1.7.6
androidx.compose.runtime:runtime-android:1.7.5 androidx.compose.runtime:runtime-android:1.8.0-alpha07
androidx.compose.runtime:runtime-saveable-android:1.7.5 androidx.compose.runtime:runtime-saveable-android:1.8.0-alpha07
androidx.compose.runtime:runtime-saveable:1.7.5 androidx.compose.runtime:runtime-saveable:1.8.0-alpha07
androidx.compose.runtime:runtime-tracing:1.7.5 androidx.compose.runtime:runtime-tracing:1.8.0-alpha07
androidx.compose.runtime:runtime:1.7.5 androidx.compose.runtime:runtime:1.8.0-alpha07
androidx.compose.ui:ui-android:1.7.5 androidx.compose.ui:ui-android:1.8.0-alpha07
androidx.compose.ui:ui-geometry-android:1.7.5 androidx.compose.ui:ui-geometry-android:1.8.0-alpha07
androidx.compose.ui:ui-geometry:1.7.5 androidx.compose.ui:ui-geometry:1.8.0-alpha07
androidx.compose.ui:ui-graphics-android:1.7.5 androidx.compose.ui:ui-graphics-android:1.8.0-alpha07
androidx.compose.ui:ui-graphics:1.7.5 androidx.compose.ui:ui-graphics:1.8.0-alpha07
androidx.compose.ui:ui-text-android:1.7.5 androidx.compose.ui:ui-text-android:1.8.0-alpha07
androidx.compose.ui:ui-text:1.7.5 androidx.compose.ui:ui-text:1.8.0-alpha07
androidx.compose.ui:ui-tooling-preview-android:1.7.5 androidx.compose.ui:ui-tooling-preview-android:1.8.0-alpha07
androidx.compose.ui:ui-tooling-preview:1.7.5 androidx.compose.ui:ui-tooling-preview:1.8.0-alpha07
androidx.compose.ui:ui-unit-android:1.7.5 androidx.compose.ui:ui-unit-android:1.8.0-alpha07
androidx.compose.ui:ui-unit:1.7.5 androidx.compose.ui:ui-unit:1.8.0-alpha07
androidx.compose.ui:ui-util-android:1.7.5 androidx.compose.ui:ui-util-android:1.8.0-alpha07
androidx.compose.ui:ui-util:1.7.5 androidx.compose.ui:ui-util:1.8.0-alpha07
androidx.compose.ui:ui:1.7.5 androidx.compose.ui:ui:1.8.0-alpha07
androidx.compose:compose-bom:2024.11.00 androidx.compose:compose-bom:2024.12.01
androidx.concurrent:concurrent-futures-ktx:1.1.0
androidx.concurrent:concurrent-futures:1.1.0 androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.13.1 androidx.core:core-ktx:1.15.0
androidx.core:core-splashscreen:1.0.1 androidx.core:core-splashscreen:1.0.1
androidx.core:core:1.13.1 androidx.core:core:1.15.0
androidx.cursoradapter:cursoradapter:1.0.0 androidx.cursoradapter:cursoradapter:1.0.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
@ -78,8 +79,8 @@ androidx.datastore:datastore-preferences:1.1.1
androidx.datastore:datastore:1.1.1 androidx.datastore:datastore:1.1.1
androidx.documentfile:documentfile:1.0.0 androidx.documentfile:documentfile:1.0.0
androidx.drawerlayout:drawerlayout:1.0.0 androidx.drawerlayout:drawerlayout:1.0.0
androidx.emoji2:emoji2-views-helper:1.3.0 androidx.emoji2:emoji2-views-helper:1.4.0
androidx.emoji2:emoji2:1.3.0 androidx.emoji2:emoji2:1.4.0
androidx.exifinterface:exifinterface:1.3.7 androidx.exifinterface:exifinterface:1.3.7
androidx.fragment:fragment:1.5.4 androidx.fragment:fragment:1.5.4
androidx.graphics:graphics-path:1.0.1 androidx.graphics:graphics-path:1.0.1
@ -89,38 +90,38 @@ androidx.hilt:hilt-navigation:1.2.0
androidx.hilt:hilt-work:1.2.0 androidx.hilt:hilt-work:1.2.0
androidx.interpolator:interpolator:1.0.0 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.8.6 androidx.lifecycle:lifecycle-common-java8:2.8.7
androidx.lifecycle:lifecycle-common-jvm:2.8.6 androidx.lifecycle:lifecycle-common-jvm:2.8.7
androidx.lifecycle:lifecycle-common:2.8.6 androidx.lifecycle:lifecycle-common:2.8.7
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.6 androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.7
androidx.lifecycle:lifecycle-livedata-core:2.8.6 androidx.lifecycle:lifecycle-livedata-core:2.8.7
androidx.lifecycle:lifecycle-livedata:2.8.6 androidx.lifecycle:lifecycle-livedata:2.8.7
androidx.lifecycle:lifecycle-process:2.8.6 androidx.lifecycle:lifecycle-process:2.8.7
androidx.lifecycle:lifecycle-runtime-android:2.8.6 androidx.lifecycle:lifecycle-runtime-android:2.8.7
androidx.lifecycle:lifecycle-runtime-compose-android:2.8.6 androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7
androidx.lifecycle:lifecycle-runtime-compose:2.8.6 androidx.lifecycle:lifecycle-runtime-compose:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.6 androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx:2.8.6 androidx.lifecycle:lifecycle-runtime-ktx:2.8.7
androidx.lifecycle:lifecycle-runtime:2.8.6 androidx.lifecycle:lifecycle-runtime:2.8.7
androidx.lifecycle:lifecycle-service:2.8.6 androidx.lifecycle:lifecycle-service:2.8.7
androidx.lifecycle:lifecycle-viewmodel-android:2.8.6 androidx.lifecycle:lifecycle-viewmodel-android:2.8.7
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.6 androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.7
androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6 androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7
androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6 androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.6 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7
androidx.lifecycle:lifecycle-viewmodel:2.8.6 androidx.lifecycle:lifecycle-viewmodel:2.8.7
androidx.loader:loader:1.0.0 androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.metrics:metrics-performance:1.0.0-beta01 androidx.metrics:metrics-performance:1.0.0-beta01
androidx.navigation:navigation-common-ktx:2.8.0 androidx.navigation:navigation-common-ktx:2.8.5
androidx.navigation:navigation-common:2.8.0 androidx.navigation:navigation-common:2.8.5
androidx.navigation:navigation-compose:2.8.0 androidx.navigation:navigation-compose:2.8.5
androidx.navigation:navigation-runtime-ktx:2.8.0 androidx.navigation:navigation-runtime-ktx:2.8.5
androidx.navigation:navigation-runtime:2.8.0 androidx.navigation:navigation-runtime:2.8.5
androidx.print:print:1.0.0 androidx.print:print:1.0.0
androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05 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.4.1
androidx.resourceinspection:resourceinspection-annotation:1.0.1 androidx.resourceinspection:resourceinspection-annotation:1.0.1
androidx.room:room-common:2.6.1 androidx.room:room-common:2.6.1
androidx.room:room-ktx:2.6.1 androidx.room:room-ktx:2.6.1
@ -141,11 +142,11 @@ androidx.window.extensions.core:core:1.0.0
androidx.window:window-core-android:1.3.0 androidx.window:window-core-android:1.3.0
androidx.window:window-core:1.3.0 androidx.window:window-core:1.3.0
androidx.window:window:1.3.0 androidx.window:window:1.3.0
androidx.work:work-runtime-ktx:2.9.0 androidx.work:work-runtime-ktx:2.10.0
androidx.work:work-runtime:2.9.0 androidx.work:work-runtime:2.10.0
com.caverock:androidsvg-aar:1.4 com.caverock:androidsvg-aar:1.4
com.google.accompanist:accompanist-drawablepainter:0.32.0 com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.accompanist:accompanist-permissions:0.34.0 com.google.accompanist:accompanist-permissions:0.37.0
com.google.android.datatransport:transport-api:3.2.0 com.google.android.datatransport:transport-api:3.2.0
com.google.android.datatransport:transport-backend-cct:3.3.0 com.google.android.datatransport:transport-backend-cct:3.3.0
com.google.android.datatransport:transport-runtime:3.3.0 com.google.android.datatransport:transport-runtime:3.3.0
@ -153,31 +154,31 @@ com.google.android.gms:play-services-ads-identifier:18.0.0
com.google.android.gms:play-services-base:18.5.0 com.google.android.gms:play-services-base:18.5.0
com.google.android.gms:play-services-basement:18.4.0 com.google.android.gms:play-services-basement:18.4.0
com.google.android.gms:play-services-cloud-messaging:17.2.0 com.google.android.gms:play-services-cloud-messaging:17.2.0
com.google.android.gms:play-services-measurement-api:22.1.0 com.google.android.gms:play-services-measurement-api:22.1.2
com.google.android.gms:play-services-measurement-base:22.1.0 com.google.android.gms:play-services-measurement-base:22.1.2
com.google.android.gms:play-services-measurement-impl:22.1.0 com.google.android.gms:play-services-measurement-impl:22.1.2
com.google.android.gms:play-services-measurement-sdk-api:22.1.0 com.google.android.gms:play-services-measurement-sdk-api:22.1.2
com.google.android.gms:play-services-measurement-sdk:22.1.0 com.google.android.gms:play-services-measurement-sdk:22.1.2
com.google.android.gms:play-services-measurement:22.1.0 com.google.android.gms:play-services-measurement:22.1.2
com.google.android.gms:play-services-oss-licenses:17.1.0 com.google.android.gms:play-services-oss-licenses:17.1.0
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.2.0 com.google.android.gms:play-services-tasks:18.2.0
com.google.code.findbugs:jsr305:3.0.2 com.google.code.findbugs:jsr305:3.0.2
com.google.dagger:dagger-lint-aar:2.52 com.google.dagger:dagger-lint-aar:2.54
com.google.dagger:dagger:2.52 com.google.dagger:dagger:2.54
com.google.dagger:hilt-android:2.52 com.google.dagger:hilt-android:2.54
com.google.dagger:hilt-core:2.52 com.google.dagger:hilt-core:2.54
com.google.errorprone:error_prone_annotations:2.26.0 com.google.errorprone:error_prone_annotations:2.26.0
com.google.firebase:firebase-abt:21.1.1 com.google.firebase:firebase-abt:21.1.1
com.google.firebase:firebase-analytics:22.1.0 com.google.firebase:firebase-analytics:22.1.2
com.google.firebase:firebase-annotations:16.2.0 com.google.firebase:firebase-annotations:16.2.0
com.google.firebase:firebase-bom:33.3.0 com.google.firebase:firebase-bom:33.7.0
com.google.firebase:firebase-common-ktx:21.0.0 com.google.firebase:firebase-common-ktx:21.0.0
com.google.firebase:firebase-common:21.0.0 com.google.firebase:firebase-common:21.0.0
com.google.firebase:firebase-components:18.0.0 com.google.firebase:firebase-components:18.0.0
com.google.firebase:firebase-config-interop:16.0.1 com.google.firebase:firebase-config-interop:16.0.1
com.google.firebase:firebase-config:22.0.0 com.google.firebase:firebase-config:22.0.1
com.google.firebase:firebase-crashlytics:19.1.0 com.google.firebase:firebase-crashlytics:19.3.0
com.google.firebase:firebase-datatransport:19.0.0 com.google.firebase:firebase-datatransport:19.0.0
com.google.firebase:firebase-encoders-json:18.0.1 com.google.firebase:firebase-encoders-json:18.0.1
com.google.firebase:firebase-encoders-proto:16.0.0 com.google.firebase:firebase-encoders-proto:16.0.0
@ -186,16 +187,15 @@ com.google.firebase:firebase-iid-interop:17.1.0
com.google.firebase:firebase-installations-interop:17.2.0 com.google.firebase:firebase-installations-interop:17.2.0
com.google.firebase:firebase-installations:18.0.0 com.google.firebase:firebase-installations:18.0.0
com.google.firebase:firebase-measurement-connector:20.0.1 com.google.firebase:firebase-measurement-connector:20.0.1
com.google.firebase:firebase-messaging:24.0.1 com.google.firebase:firebase-messaging:24.1.0
com.google.firebase:firebase-perf:21.0.1 com.google.firebase:firebase-perf:21.0.3
com.google.firebase:firebase-sessions:2.0.4 com.google.firebase:firebase-sessions:2.0.7
com.google.firebase:protolite-well-known-types:18.0.0
com.google.guava:failureaccess:1.0.1 com.google.guava:failureaccess:1.0.1
com.google.guava:guava:31.1-android com.google.guava:guava:31.1-android
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
com.google.j2objc:j2objc-annotations:1.3 com.google.j2objc:j2objc-annotations:1.3
com.google.protobuf:protobuf-javalite:4.26.1 com.google.protobuf:protobuf-javalite:4.29.2
com.google.protobuf:protobuf-kotlin-lite:4.26.1 com.google.protobuf:protobuf-kotlin-lite:4.29.2
com.squareup.okhttp3:logging-interceptor:4.12.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.9.0 com.squareup.okio:okio-jvm:3.9.0
@ -212,21 +212,22 @@ javax.inject:javax.inject:1
org.checkerframework:checker-qual:3.12.0 org.checkerframework:checker-qual:3.12.0
org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.22 org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.22
org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.22 org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.22
org.jetbrains.kotlin:kotlin-stdlib-common:2.0.20 org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
org.jetbrains.kotlin:kotlin-stdlib:2.0.20 org.jetbrains.kotlin:kotlin-stdlib:2.1.0
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.1
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.1
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1
org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.9.0 org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.1
org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.9.0 org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.1
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1 org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1
org.jetbrains.kotlinx:kotlinx-datetime:0.6.1 org.jetbrains.kotlinx:kotlinx-datetime:0.6.1
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3 org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.0
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.3 org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.8.0
org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.0
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.3 org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.8.0
org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3 org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0
org.jetbrains:annotations:23.0.0 org.jetbrains:annotations:23.0.0
org.jspecify:jspecify:1.0.0

@ -1,6 +1,6 @@
package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='14' platformBuildVersionCode='34' compileSdkVersion='34' compileSdkVersionCodename='14' package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
sdkVersion:'21' sdkVersion:'21'
targetSdkVersion:'34' targetSdkVersion:'35'
uses-permission: name='android.permission.INTERNET' uses-permission: name='android.permission.INTERNET'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE' uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.POST_NOTIFICATIONS' uses-permission: name='android.permission.POST_NOTIFICATIONS'

@ -41,6 +41,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="uiMode"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

@ -22,10 +22,7 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -35,21 +32,22 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.metrics.performance.JankStats import androidx.metrics.performance.JankStats
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.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.data.util.TimeZoneMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.ui.LocalTimeZone import com.google.samples.apps.nowinandroid.core.ui.LocalTimeZone
import com.google.samples.apps.nowinandroid.ui.NiaApp import com.google.samples.apps.nowinandroid.ui.NiaApp
import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState
import com.google.samples.apps.nowinandroid.util.isSystemInDarkTheme
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -81,53 +79,60 @@ class MainActivity : ComponentActivity() {
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
var uiState: MainActivityUiState by mutableStateOf(Loading) // We keep this as a mutable state, so that we can track changes inside the composition.
// This allows us to react to dark/light mode changes.
var themeSettings by mutableStateOf(
ThemeSettings(
darkTheme = resources.configuration.isSystemInDarkTheme,
androidTheme = Loading.shouldUseAndroidTheme,
disableDynamicTheming = Loading.shouldDisableDynamicTheming,
),
)
// Update the uiState // Update the uiState
lifecycleScope.launch { lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState combine(
.onEach { uiState = it } isSystemInDarkTheme(),
.collect() viewModel.uiState,
} ) { systemDark, uiState ->
} ThemeSettings(
darkTheme = uiState.shouldUseDarkTheme(systemDark),
// Keep the splash screen on-screen until the UI state is loaded. This condition is androidTheme = uiState.shouldUseAndroidTheme,
// evaluated each time the app needs to be redrawn so it should be fast to avoid blocking disableDynamicTheming = uiState.shouldDisableDynamicTheming,
// the UI. )
splashScreen.setKeepOnScreenCondition {
when (uiState) {
Loading -> true
is Success -> false
}
} }
.onEach { themeSettings = it }
.map { it.darkTheme }
.distinctUntilChanged()
.collect { darkTheme ->
trace("niaEdgeToEdge") {
// Turn off the decor fitting system windows, which allows us to handle insets, // Turn off the decor fitting system windows, which allows us to handle insets,
// including IME animations, and go edge-to-edge // including IME animations, and go edge-to-edge.
// This also sets up the initial system bar style based on the platform theme
enableEdgeToEdge()
setContent {
val darkTheme = shouldUseDarkTheme(uiState)
// Update the edge to edge configuration to match the theme
// This is the same parameters as the default enableEdgeToEdge call, but we manually // This is the same parameters as the default enableEdgeToEdge call, but we manually
// resolve whether or not to show dark theme using uiState, since it can be different // resolve whether or not to show dark theme using uiState, since it can be different
// than the configuration's dark theme value based on the user preference. // than the configuration's dark theme value based on the user preference.
DisposableEffect(darkTheme) {
enableEdgeToEdge( enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto( statusBarStyle = SystemBarStyle.auto(
android.graphics.Color.TRANSPARENT, lightScrim = android.graphics.Color.TRANSPARENT,
android.graphics.Color.TRANSPARENT, darkScrim = android.graphics.Color.TRANSPARENT,
) { darkTheme }, ) { darkTheme },
navigationBarStyle = SystemBarStyle.auto( navigationBarStyle = SystemBarStyle.auto(
lightScrim, lightScrim = lightScrim,
darkScrim, darkScrim = darkScrim,
) { darkTheme }, ) { darkTheme },
) )
onDispose {}
} }
}
}
}
// Keep the splash screen on-screen until the UI state is loaded. This condition is
// evaluated each time the app needs to be redrawn so it should be fast to avoid blocking
// the UI.
splashScreen.setKeepOnScreenCondition { viewModel.uiState.value.shouldKeepSplashScreen() }
setContent {
val appState = rememberNiaAppState( val appState = rememberNiaAppState(
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
@ -141,9 +146,9 @@ class MainActivity : ComponentActivity() {
LocalTimeZone provides currentTimeZone, LocalTimeZone provides currentTimeZone,
) { ) {
NiaTheme( NiaTheme(
darkTheme = darkTheme, darkTheme = themeSettings.darkTheme,
androidTheme = shouldUseAndroidTheme(uiState), androidTheme = themeSettings.androidTheme,
disableDynamicTheming = shouldDisableDynamicTheming(uiState), disableDynamicTheming = themeSettings.disableDynamicTheming,
) { ) {
NiaApp(appState) NiaApp(appState)
} }
@ -162,47 +167,6 @@ class MainActivity : ComponentActivity() {
} }
} }
/**
* Returns `true` if the Android theme should be used, as a function of the [uiState].
*/
@Composable
private fun shouldUseAndroidTheme(
uiState: MainActivityUiState,
): Boolean = when (uiState) {
Loading -> false
is Success -> when (uiState.userData.themeBrand) {
ThemeBrand.DEFAULT -> false
ThemeBrand.ANDROID -> true
}
}
/**
* Returns `true` if the dynamic color is disabled, as a function of the [uiState].
*/
@Composable
private fun shouldDisableDynamicTheming(
uiState: MainActivityUiState,
): Boolean = when (uiState) {
Loading -> false
is Success -> !uiState.userData.useDynamicColor
}
/**
* Returns `true` if dark theme should be used, as a function of the [uiState] and the
* current system context.
*/
@Composable
private fun shouldUseDarkTheme(
uiState: MainActivityUiState,
): Boolean = when (uiState) {
Loading -> isSystemInDarkTheme()
is Success -> when (uiState.userData.darkThemeConfig) {
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
DarkThemeConfig.LIGHT -> false
DarkThemeConfig.DARK -> true
}
}
/** /**
* The default light scrim, as defined by androidx and the platform: * The default light scrim, as defined by androidx and the platform:
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598 * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
@ -214,3 +178,13 @@ private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598 * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
*/ */
private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b) private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)
/**
* Class for the system theme settings.
* This wrapping class allows us to combine all the changes and prevent unnecessary recompositions.
*/
data class ThemeSettings(
val darkTheme: Boolean,
val androidTheme: Boolean,
val disableDynamicTheming: Boolean,
)

@ -21,6 +21,8 @@ import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData import com.google.samples.apps.nowinandroid.core.model.data.UserData
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@ -44,5 +46,40 @@ class MainActivityViewModel @Inject constructor(
sealed interface MainActivityUiState { sealed interface MainActivityUiState {
data object Loading : MainActivityUiState data object Loading : MainActivityUiState
data class Success(val userData: UserData) : MainActivityUiState
data class Success(val userData: UserData) : MainActivityUiState {
override val shouldDisableDynamicTheming = !userData.useDynamicColor
override val shouldUseAndroidTheme: Boolean = when (userData.themeBrand) {
ThemeBrand.DEFAULT -> false
ThemeBrand.ANDROID -> true
}
override fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) =
when (userData.darkThemeConfig) {
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemDarkTheme
DarkThemeConfig.LIGHT -> false
DarkThemeConfig.DARK -> true
}
}
/**
* Returns `true` if the state wasn't loaded yet and it should keep showing the splash screen.
*/
fun shouldKeepSplashScreen() = this is Loading
/**
* Returns `true` if the dynamic color is disabled.
*/
val shouldDisableDynamicTheming: Boolean get() = true
/**
* Returns `true` if the Android theme should be used.
*/
val shouldUseAndroidTheme: Boolean get() = false
/**
* Returns `true` if dark theme should be used.
*/
fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) = isSystemDarkTheme
} }

@ -17,6 +17,9 @@
package com.google.samples.apps.nowinandroid package com.google.samples.apps.nowinandroid
import android.app.Application import android.app.Application
import android.content.pm.ApplicationInfo
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy.Builder
import coil.ImageLoader import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import com.google.samples.apps.nowinandroid.sync.initializers.Sync import com.google.samples.apps.nowinandroid.sync.initializers.Sync
@ -37,10 +40,34 @@ class NiaApplication : Application(), ImageLoaderFactory {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
setStrictModePolicy()
// Initialize Sync; the system responsible for keeping data in the app up to date. // Initialize Sync; the system responsible for keeping data in the app up to date.
Sync.initialize(context = this) Sync.initialize(context = this)
profileVerifierLogger() profileVerifierLogger()
} }
override fun newImageLoader(): ImageLoader = imageLoader.get() override fun newImageLoader(): ImageLoader = imageLoader.get()
/**
* Return true if the application is debuggable.
*/
private fun isDebuggable(): Boolean {
return 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
}
/**
* Set a thread policy that detects all potential problems on the main thread, such as network
* and disk access.
*
* If a problem is found, the offending call will be logged and the application will be killed.
*/
private fun setStrictModePolicy() {
if (isDebuggable()) {
StrictMode.setThreadPolicy(
Builder().detectAll().penaltyLog().penaltyDeath().build(),
)
}
}
} }

@ -37,7 +37,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.material3.SnackbarResult.ActionPerformed
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -76,7 +75,6 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
import kotlin.reflect.KClass import kotlin.reflect.KClass
import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable @Composable
fun NiaApp( fun NiaApp(
appState: NiaAppState, appState: NiaAppState,
@ -126,7 +124,6 @@ fun NiaApp(
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalComposeUiApi::class, ExperimentalComposeUiApi::class,
ExperimentalMaterial3AdaptiveApi::class,
) )
internal fun NiaApp( internal fun NiaApp(
appState: NiaAppState, appState: NiaAppState,

@ -18,6 +18,8 @@ package com.google.samples.apps.nowinandroid.ui
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavController import androidx.navigation.NavController
@ -25,7 +27,6 @@ import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions import androidx.navigation.navOptions
import androidx.tracing.trace import androidx.tracing.trace
@ -83,9 +84,21 @@ class NiaAppState(
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor, timeZoneMonitor: TimeZoneMonitor,
) { ) {
private val previousDestination = mutableStateOf<NavDestination?>(null)
val currentDestination: NavDestination? val currentDestination: NavDestination?
@Composable get() = navController @Composable get() {
.currentBackStackEntryAsState().value?.destination // Collect the currentBackStackEntryFlow as a state
val currentEntry = navController.currentBackStackEntryFlow
.collectAsState(initial = null)
// Fallback to previousDestination if currentEntry is null
return currentEntry.value?.destination.also { destination ->
if (destination != null) {
previousDestination.value = destination
}
} ?: previousDestination.value
}
val currentTopLevelDestination: TopLevelDestination? val currentTopLevelDestination: TopLevelDestination?
@Composable get() { @Composable get() {

@ -0,0 +1,49 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.util
import android.content.res.Configuration
import androidx.activity.ComponentActivity
import androidx.core.util.Consumer
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
/**
* Convenience wrapper for dark mode checking
*/
val Configuration.isSystemInDarkTheme
get() = (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
/**
* Registers listener for configuration changes to retrieve whether system is in dark theme or not.
* Immediately upon subscribing, it sends the current value and then registers listener for changes.
*/
fun ComponentActivity.isSystemInDarkTheme() = callbackFlow {
channel.trySend(resources.configuration.isSystemInDarkTheme)
val listener = Consumer<Configuration> {
channel.trySend(it.isSystemInDarkTheme)
}
addOnConfigurationChangedListener(listener)
awaitClose { removeOnConfigurationChangedListener(listener) }
}
.distinctUntilChanged()
.conflate()

@ -21,7 +21,9 @@
<style name="NightAdjusted.Theme.Nia" parent="android:Theme.Material.Light.NoActionBar" /> <style name="NightAdjusted.Theme.Nia" parent="android:Theme.Material.Light.NoActionBar" />
<!-- The final theme we use --> <!-- The final theme we use -->
<style name="Theme.Nia" parent="NightAdjusted.Theme.Nia" /> <style name="Theme.Nia" parent="NightAdjusted.Theme.Nia">
<item name="android:forceDarkAllowed" tools:targetApi="29">false</item>
</style>
<style name="NightAdjusted.Theme.Splash" parent="Theme.SplashScreen"> <style name="NightAdjusted.Theme.Splash" parent="Theme.SplashScreen">
<item name="android:windowLightStatusBar" tools:targetApi="23">true</item> <item name="android:windowLightStatusBar" tools:targetApi="23">true</item>

@ -16,7 +16,6 @@
package com.google.samples.apps.nowinandroid.ui package com.google.samples.apps.nowinandroid.ui
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.Posture import androidx.compose.material3.adaptive.Posture
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@ -113,7 +112,6 @@ class NiaAppScreenSizesScreenshotTests {
TimeZone.setDefault(TimeZone.getTimeZone("UTC")) TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
} }
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun testNiaAppScreenshotWithSize(width: Dp, height: Dp, screenshotName: String) { private fun testNiaAppScreenshotWithSize(width: Dp, height: Dp, screenshotName: String) {
composeTestRule.setContent { composeTestRule.setContent {
CompositionLocalProvider( CompositionLocalProvider(

@ -31,6 +31,8 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepo
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
@ -38,15 +40,18 @@ import kotlinx.coroutines.test.runTest
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
/** /**
* Tests [NiaAppState]. * Tests [NiaAppState].
*
* Note: This could become an unit test if Robolectric is added to the project and the Context
* is faked.
*/ */
@RunWith(RobolectricTestRunner::class)
@Config(application = HiltTestApplication::class)
@HiltAndroidTest
class NiaAppStateTest { class NiaAppStateTest {
@get:Rule @get:Rule

@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.windowInsetsStartWidth
import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarDuration.Indefinite
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.Posture import androidx.compose.material3.adaptive.Posture
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -209,7 +208,6 @@ class SnackbarInsetsScreenshotTests {
} }
} }
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun testSnackbarScreenshotWithSize( private fun testSnackbarScreenshotWithSize(
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
width: Dp, width: Dp,

@ -19,7 +19,6 @@ package com.google.samples.apps.nowinandroid.ui
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarDuration.Indefinite
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.Posture import androidx.compose.material3.adaptive.Posture
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@ -182,7 +181,6 @@ class SnackbarScreenshotTests {
} }
} }
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun testSnackbarScreenshotWithSize( private fun testSnackbarScreenshotWithSize(
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
width: Dp, width: Dp,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 97 KiB

@ -15,10 +15,10 @@
*/ */
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
`kotlin-dsl` `kotlin-dsl`
alias(libs.plugins.android.lint)
} }
group = "com.google.samples.apps.nowinandroid.buildlogic" group = "com.google.samples.apps.nowinandroid.buildlogic"
@ -46,6 +46,7 @@ dependencies {
compileOnly(libs.ksp.gradlePlugin) compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.room.gradlePlugin) compileOnly(libs.room.gradlePlugin)
implementation(libs.truth) implementation(libs.truth)
lintChecks(libs.androidx.lint.gradle)
} }
tasks { tasks {
@ -58,59 +59,59 @@ tasks {
gradlePlugin { gradlePlugin {
plugins { plugins {
register("androidApplicationCompose") { register("androidApplicationCompose") {
id = "nowinandroid.android.application.compose" id = libs.plugins.nowinandroid.android.application.compose.get().pluginId
implementationClass = "AndroidApplicationComposeConventionPlugin" implementationClass = "AndroidApplicationComposeConventionPlugin"
} }
register("androidApplication") { register("androidApplication") {
id = "nowinandroid.android.application" id = libs.plugins.nowinandroid.android.application.asProvider().get().pluginId
implementationClass = "AndroidApplicationConventionPlugin" implementationClass = "AndroidApplicationConventionPlugin"
} }
register("androidApplicationJacoco") { register("androidApplicationJacoco") {
id = "nowinandroid.android.application.jacoco" id = libs.plugins.nowinandroid.android.application.jacoco.get().pluginId
implementationClass = "AndroidApplicationJacocoConventionPlugin" implementationClass = "AndroidApplicationJacocoConventionPlugin"
} }
register("androidLibraryCompose") { register("androidLibraryCompose") {
id = "nowinandroid.android.library.compose" id = libs.plugins.nowinandroid.android.library.compose.get().pluginId
implementationClass = "AndroidLibraryComposeConventionPlugin" implementationClass = "AndroidLibraryComposeConventionPlugin"
} }
register("androidLibrary") { register("androidLibrary") {
id = "nowinandroid.android.library" id = libs.plugins.nowinandroid.android.library.asProvider().get().pluginId
implementationClass = "AndroidLibraryConventionPlugin" implementationClass = "AndroidLibraryConventionPlugin"
} }
register("androidFeature") { register("androidFeature") {
id = "nowinandroid.android.feature" id = libs.plugins.nowinandroid.android.feature.get().pluginId
implementationClass = "AndroidFeatureConventionPlugin" implementationClass = "AndroidFeatureConventionPlugin"
} }
register("androidLibraryJacoco") { register("androidLibraryJacoco") {
id = "nowinandroid.android.library.jacoco" id = libs.plugins.nowinandroid.android.library.jacoco.get().pluginId
implementationClass = "AndroidLibraryJacocoConventionPlugin" implementationClass = "AndroidLibraryJacocoConventionPlugin"
} }
register("androidTest") { register("androidTest") {
id = "nowinandroid.android.test" id = libs.plugins.nowinandroid.android.test.get().pluginId
implementationClass = "AndroidTestConventionPlugin" implementationClass = "AndroidTestConventionPlugin"
} }
register("hilt") { register("hilt") {
id = "nowinandroid.hilt" id = libs.plugins.nowinandroid.hilt.get().pluginId
implementationClass = "HiltConventionPlugin" implementationClass = "HiltConventionPlugin"
} }
register("androidRoom") { register("androidRoom") {
id = "nowinandroid.android.room" id = libs.plugins.nowinandroid.android.room.get().pluginId
implementationClass = "AndroidRoomConventionPlugin" implementationClass = "AndroidRoomConventionPlugin"
} }
register("androidFirebase") { register("androidFirebase") {
id = "nowinandroid.android.application.firebase" id = libs.plugins.nowinandroid.android.application.firebase.get().pluginId
implementationClass = "AndroidApplicationFirebaseConventionPlugin" implementationClass = "AndroidApplicationFirebaseConventionPlugin"
} }
register("androidFlavors") { register("androidFlavors") {
id = "nowinandroid.android.application.flavors" id = libs.plugins.nowinandroid.android.application.flavors.get().pluginId
implementationClass = "AndroidApplicationFlavorsConventionPlugin" implementationClass = "AndroidApplicationFlavorsConventionPlugin"
} }
register("androidLint") { register("androidLint") {
id = "nowinandroid.android.lint" id = libs.plugins.nowinandroid.android.lint.get().pluginId
implementationClass = "AndroidLintConventionPlugin" implementationClass = "AndroidLintConventionPlugin"
} }
register("jvmLibrary") { register("jvmLibrary") {
id = "nowinandroid.jvm.library" id = libs.plugins.nowinandroid.jvm.library.get().pluginId
implementationClass = "JvmLibraryConventionPlugin" implementationClass = "JvmLibraryConventionPlugin"
} }
} }

@ -23,22 +23,21 @@ import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask import com.google.samples.apps.nowinandroid.configurePrintApksTask
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.getByType
class AndroidApplicationConventionPlugin : Plugin<Project> { class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
with(pluginManager) { apply(plugin = "com.android.application")
apply("com.android.application") apply(plugin = "org.jetbrains.kotlin.android")
apply("org.jetbrains.kotlin.android") apply(plugin = "nowinandroid.android.lint")
apply("nowinandroid.android.lint") apply(plugin = "com.dropbox.dependency-guard")
apply("com.dropbox.dependency-guard")
}
extensions.configure<ApplicationExtension> { extensions.configure<ApplicationExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig.targetSdk = 34 defaultConfig.targetSdk = 35
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
testOptions.animationsDisabled = true testOptions.animationsDisabled = true
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
@ -49,5 +48,4 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
} }
} }
} }
} }

@ -19,23 +19,32 @@ import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
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.kotlin.dsl.apply
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.exclude
class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> { class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
with(pluginManager) { apply(plugin = "com.google.gms.google-services")
apply("com.google.gms.google-services") apply(plugin = "com.google.firebase.firebase-perf")
apply("com.google.firebase.firebase-perf") apply(plugin = "com.google.firebase.crashlytics")
apply("com.google.firebase.crashlytics")
}
dependencies { dependencies {
val bom = libs.findLibrary("firebase-bom").get() val bom = libs.findLibrary("firebase-bom").get()
add("implementation", platform(bom)) "implementation"(platform(bom))
"implementation"(libs.findLibrary("firebase.analytics").get()) "implementation"(libs.findLibrary("firebase.analytics").get())
"implementation"(libs.findLibrary("firebase.performance").get()) "implementation"(libs.findLibrary("firebase.performance").get()) {
/*
Exclusion of protobuf / protolite dependencies is necessary as the
datastore-proto brings in protobuf dependencies. These are the source of truth
for Now in Android.
That's why the duplicate classes from below dependencies are excluded.
*/
exclude(group = "com.google.protobuf", module = "protobuf-javalite")
exclude(group = "com.google.firebase", module = "protolite-well-known-types")
}
"implementation"(libs.findLibrary("firebase.crashlytics").get()) "implementation"(libs.findLibrary("firebase.crashlytics").get())
} }

@ -14,18 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import com.google.samples.apps.nowinandroid.configureJacoco import com.google.samples.apps.nowinandroid.configureJacoco
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.getByType
class AndroidApplicationJacocoConventionPlugin : Plugin<Project> { class AndroidApplicationJacocoConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
pluginManager.apply("jacoco") apply(plugin = "jacoco")
val androidExtension = extensions.getByType<BaseAppModuleExtension>()
val androidExtension = extensions.getByType<ApplicationExtension>()
androidExtension.buildTypes.configureEach { androidExtension.buildTypes.configureEach {
enableAndroidTestCoverage = true enableAndroidTestCoverage = true

@ -19,35 +19,37 @@ import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
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.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
class AndroidFeatureConventionPlugin : Plugin<Project> { class AndroidFeatureConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
pluginManager.apply { apply(plugin = "nowinandroid.android.library")
apply("nowinandroid.android.library") apply(plugin = "nowinandroid.hilt")
apply("nowinandroid.hilt") apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
apply("org.jetbrains.kotlin.plugin.serialization")
}
extensions.configure<LibraryExtension> { extensions.configure<LibraryExtension> {
testOptions.animationsDisabled = true testOptions.animationsDisabled = true
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
} }
dependencies { dependencies {
add("implementation", project(":core:ui")) "implementation"(project(":core:ui"))
add("implementation", project(":core:designsystem")) "implementation"(project(":core:designsystem"))
add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get()) "implementation"(libs.findLibrary("androidx.hilt.navigation.compose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) "implementation"(libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) "implementation"(libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
add("implementation", libs.findLibrary("androidx.navigation.compose").get()) "implementation"(libs.findLibrary("androidx.navigation.compose").get())
add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) "implementation"(libs.findLibrary("androidx.tracing.ktx").get())
add("implementation", libs.findLibrary("kotlinx.serialization.json").get()) "implementation"(libs.findLibrary("kotlinx.serialization.json").get())
add("testImplementation", libs.findLibrary("androidx.navigation.testing").get()) "testImplementation"(libs.findLibrary("androidx.navigation.testing").get())
add("androidTestImplementation", libs.findLibrary("androidx.lifecycle.runtimeTesting").get()) "androidTestImplementation"(
libs.findLibrary("androidx.lifecycle.runtimeTesting").get(),
)
} }
} }
} }

@ -24,39 +24,39 @@ import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests
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.kotlin.dsl.apply
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 AndroidLibraryConventionPlugin : Plugin<Project> { class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
with(pluginManager) { apply(plugin = "com.android.library")
apply("com.android.library") apply(plugin = "org.jetbrains.kotlin.android")
apply("org.jetbrains.kotlin.android") apply(plugin = "nowinandroid.android.lint")
apply("nowinandroid.android.lint")
}
extensions.configure<LibraryExtension> { extensions.configure<LibraryExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig.targetSdk = 34 defaultConfig.targetSdk = 35
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testOptions.animationsDisabled = true testOptions.animationsDisabled = true
configureFlavors(this) configureFlavors(this)
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
// The resource prefix is derived from the module name, // The resource prefix is derived from the module name,
// so resources inside ":core:module1" must be prefixed with "core_module1_" // so resources inside ":core:module1" must be prefixed with "core_module1_"
resourcePrefix = path.split("""\W""".toRegex()).drop(1).distinct().joinToString(separator = "_").lowercase() + "_" resourcePrefix =
path.split("""\W""".toRegex()).drop(1).distinct().joinToString(separator = "_")
.lowercase() + "_"
} }
extensions.configure<LibraryAndroidComponentsExtension> { extensions.configure<LibraryAndroidComponentsExtension> {
configurePrintApksTask(this) configurePrintApksTask(this)
disableUnnecessaryAndroidTests(target) disableUnnecessaryAndroidTests(target)
} }
dependencies { dependencies {
add("androidTestImplementation", kotlin("test")) "androidTestImplementation"(libs.findLibrary("kotlin.test").get())
add("testImplementation", kotlin("test")) "testImplementation"(libs.findLibrary("kotlin.test").get())
add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) "implementation"(libs.findLibrary("androidx.tracing.ktx").get())
} }
} }
} }

@ -15,17 +15,18 @@
*/ */
import com.android.build.api.dsl.LibraryExtension import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.google.samples.apps.nowinandroid.configureJacoco import com.google.samples.apps.nowinandroid.configureJacoco
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.getByType
class AndroidLibraryJacocoConventionPlugin : Plugin<Project> { class AndroidLibraryJacocoConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
pluginManager.apply("jacoco") apply(plugin = "jacoco")
val androidExtension = extensions.getByType<LibraryExtension>() val androidExtension = extensions.getByType<LibraryExtension>()
androidExtension.buildTypes.configureEach { androidExtension.buildTypes.configureEach {

@ -19,6 +19,7 @@ import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.dsl.Lint import com.android.build.api.dsl.Lint
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
class AndroidLintConventionPlugin : Plugin<Project> { class AndroidLintConventionPlugin : Plugin<Project> {
@ -32,7 +33,7 @@ class AndroidLintConventionPlugin : Plugin<Project> {
configure<LibraryExtension> { lint(Lint::configure) } configure<LibraryExtension> { lint(Lint::configure) }
else -> { else -> {
pluginManager.apply("com.android.lint") apply(plugin = "com.android.lint")
configure<Lint>(Lint::configure) configure<Lint>(Lint::configure)
} }
} }
@ -42,5 +43,7 @@ class AndroidLintConventionPlugin : Plugin<Project> {
private fun Lint.configure() { private fun Lint.configure() {
xmlReport = true xmlReport = true
sarifReport = true
checkDependencies = true checkDependencies = true
disable += "GradleDependency"
} }

@ -19,6 +19,7 @@ import com.google.devtools.ksp.gradle.KspExtension
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.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
@ -26,8 +27,8 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
pluginManager.apply("androidx.room") apply(plugin = "androidx.room")
pluginManager.apply("com.google.devtools.ksp") apply(plugin = "com.google.devtools.ksp")
extensions.configure<KspExtension> { extensions.configure<KspExtension> {
arg("room.generateKotlin", "true") arg("room.generateKotlin", "true")
@ -41,9 +42,9 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
} }
dependencies { dependencies {
add("implementation", libs.findLibrary("room.runtime").get()) "implementation"(libs.findLibrary("room.runtime").get())
add("implementation", libs.findLibrary("room.ktx").get()) "implementation"(libs.findLibrary("room.ktx").get())
add("ksp", libs.findLibrary("room.compiler").get()) "ksp"(libs.findLibrary("room.compiler").get())
} }
} }
} }

@ -19,22 +19,20 @@ import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
class AndroidTestConventionPlugin : Plugin<Project> { class AndroidTestConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
with(pluginManager) { apply(plugin = "com.android.test")
apply("com.android.test") apply(plugin = "org.jetbrains.kotlin.android")
apply("org.jetbrains.kotlin.android")
}
extensions.configure<TestExtension> { extensions.configure<TestExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig.targetSdk = 34 defaultConfig.targetSdk = 35
configureGradleManagedDevices(this) configureGradleManagedDevices(this)
} }
} }
} }
} }

@ -18,28 +18,30 @@ import com.android.build.gradle.api.AndroidBasePlugin
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.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
class HiltConventionPlugin : Plugin<Project> { class HiltConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
pluginManager.apply("com.google.devtools.ksp") apply(plugin = "com.google.devtools.ksp")
dependencies { dependencies {
add("ksp", libs.findLibrary("hilt.compiler").get()) "ksp"(libs.findLibrary("hilt.compiler").get())
} }
// Add support for Jvm Module, base on org.jetbrains.kotlin.jvm // Add support for Jvm Module, base on org.jetbrains.kotlin.jvm
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
dependencies { dependencies {
add("implementation", libs.findLibrary("hilt.core").get()) "implementation"(libs.findLibrary("hilt.core").get())
} }
} }
/** Add support for Android modules, based on [AndroidBasePlugin] */ /** Add support for Android modules, based on [AndroidBasePlugin] */
pluginManager.withPlugin("com.android.base") { pluginManager.withPlugin("com.android.base") {
pluginManager.apply("dagger.hilt.android.plugin") apply(plugin = "dagger.hilt.android.plugin")
dependencies { dependencies {
add("implementation", libs.findLibrary("hilt.android").get()) "implementation"(libs.findLibrary("hilt.android").get())
} }
} }
} }

@ -15,21 +15,21 @@
*/ */
import com.google.samples.apps.nowinandroid.configureKotlinJvm import com.google.samples.apps.nowinandroid.configureKotlinJvm
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.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.kotlin
class JvmLibraryConventionPlugin : Plugin<Project> { class JvmLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
with(target) { with(target) {
with(pluginManager) { apply(plugin = "org.jetbrains.kotlin.jvm")
apply("org.jetbrains.kotlin.jvm") apply(plugin = "nowinandroid.android.lint")
apply("nowinandroid.android.lint")
}
configureKotlinJvm() configureKotlinJvm()
dependencies { dependencies {
add("testImplementation", kotlin("test")) "testImplementation"(libs.findLibrary("kotlin.test").get())
} }
} }
} }

@ -37,10 +37,10 @@ internal fun Project.configureAndroidCompose(
dependencies { dependencies {
val bom = libs.findLibrary("androidx-compose-bom").get() val bom = libs.findLibrary("androidx-compose-bom").get()
add("implementation", platform(bom)) "implementation"(platform(bom))
add("androidTestImplementation", platform(bom)) "androidTestImplementation"(platform(bom))
add("implementation", libs.findLibrary("androidx-compose-ui-tooling-preview").get()) "implementation"(libs.findLibrary("androidx-compose-ui-tooling-preview").get())
add("debugImplementation", libs.findLibrary("androidx-compose-ui-tooling").get()) "debugImplementation"(libs.findLibrary("androidx-compose-ui-tooling").get())
} }
testOptions { testOptions {
@ -53,8 +53,10 @@ internal fun Project.configureAndroidCompose(
extensions.configure<ComposeCompilerGradlePluginExtension> { extensions.configure<ComposeCompilerGradlePluginExtension> {
fun Provider<String>.onlyIfTrue() = flatMap { provider { it.takeIf(String::toBoolean) } } fun Provider<String>.onlyIfTrue() = flatMap { provider { it.takeIf(String::toBoolean) } }
fun Provider<*>.relativeToRootProject(dir: String) = flatMap { fun Provider<*>.relativeToRootProject(dir: String) = map {
rootProject.layout.buildDirectory.dir(projectDir.toRelativeString(rootDir)) isolated.rootProject.projectDirectory
.dir("build")
.dir(projectDir.toRelativeString(rootDir))
}.map { it.dir(dir) } }.map { it.dir(dir) }
project.providers.gradleProperty("enableComposeCompilerMetrics").onlyIfTrue() project.providers.gradleProperty("enableComposeCompilerMetrics").onlyIfTrue()
@ -65,7 +67,7 @@ internal fun Project.configureAndroidCompose(
.relativeToRootProject("compose-reports") .relativeToRootProject("compose-reports")
.let(reportsDestination::set) .let(reportsDestination::set)
stabilityConfigurationFile = stabilityConfigurationFiles
rootProject.layout.projectDirectory.file("compose_compiler_config.conf") .add(isolated.rootProject.projectDirectory.file("compose_compiler_config.conf"))
} }
} }

@ -30,6 +30,6 @@ import org.gradle.api.Project
internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests( internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
project: Project, project: Project,
) = beforeVariants { ) = beforeVariants {
it.enableAndroidTest = it.enableAndroidTest it.androidTest.enable = it.androidTest.enable
&& project.projectDir.resolve("src/androidTest").exists() && project.projectDir.resolve("src/androidTest").exists()
} }

@ -39,8 +39,6 @@ import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.register 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.util.Locale
import javax.inject.Inject import javax.inject.Inject
@CacheableTask @CacheableTask
@ -123,15 +121,17 @@ fun Project.configureBadgingTasks(
val generateBadging = val generateBadging =
tasks.register<GenerateBadgingTask>(generateBadgingTaskName) { tasks.register<GenerateBadgingTask>(generateBadgingTaskName) {
apk = variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE) apk = variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE)
aapt2Executable.set(
aapt2Executable = File( // TODO: Replace with `sdkComponents.aapt2` when it's available in AGP
baseExtension.sdkDirectory, // https://issuetracker.google.com/issues/376815836
componentsExtension.sdkComponents.sdkDirectory.map { directory ->
directory.file(
"${SdkConstants.FD_BUILD_TOOLS}/" + "${SdkConstants.FD_BUILD_TOOLS}/" +
"${baseExtension.buildToolsVersion}/" + "${baseExtension.buildToolsVersion}/" +
SdkConstants.FN_AAPT2, SdkConstants.FN_AAPT2,
) )
}
)
badging = project.layout.buildDirectory.file( badging = project.layout.buildDirectory.file(
"outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt", "outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt",
) )
@ -140,7 +140,7 @@ fun Project.configureBadgingTasks(
val updateBadgingTaskName = "update${capitalizedVariantName}Badging" val updateBadgingTaskName = "update${capitalizedVariantName}Badging"
tasks.register<Copy>(updateBadgingTaskName) { tasks.register<Copy>(updateBadgingTaskName) {
from(generateBadging.get().badging) from(generateBadging.map(GenerateBadgingTask::badging))
into(project.layout.projectDirectory) into(project.layout.projectDirectory)
} }
@ -148,7 +148,7 @@ fun Project.configureBadgingTasks(
tasks.register<CheckBadgingTask>(checkBadgingTaskName) { tasks.register<CheckBadgingTask>(checkBadgingTaskName) {
goldenBadging = project.layout.projectDirectory.file("${variant.name}-badging.txt") goldenBadging = project.layout.projectDirectory.file("${variant.name}-badging.txt")
generatedBadging = generateBadging.get().badging generatedBadging.set(generateBadging.flatMap(GenerateBadgingTask::badging))
this.updateBadgingTaskName = updateBadgingTaskName this.updateBadgingTaskName = updateBadgingTaskName

@ -19,10 +19,12 @@ package com.google.samples.apps.nowinandroid
import com.android.build.api.artifact.ScopedArtifact import com.android.build.api.artifact.ScopedArtifact
import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ScopedArtifacts import com.android.build.api.variant.ScopedArtifacts
import com.android.build.api.variant.SourceDirectories
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.file.Directory import org.gradle.api.file.Directory
import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFile
import org.gradle.api.provider.ListProperty import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
@ -88,11 +90,14 @@ internal fun Project.configureJacoco(
html.required = true html.required = true
} }
// TODO: This is missing files in src/debug/, src/prod, src/demo, src/demoDebug... fun SourceDirectories.Flat?.toFilePaths(): Provider<List<String>> = this
?.all
?.map { directories -> directories.map { it.asFile.path } }
?: provider { emptyList() }
sourceDirectories.setFrom( sourceDirectories.setFrom(
files( files(
"$projectDir/src/main/java", variant.sources.java.toFilePaths(),
"$projectDir/src/main/kotlin", variant.sources.kotlin.toFilePaths()
), ),
) )

@ -26,8 +26,8 @@ import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.provideDelegate
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension
/** /**
* Configure base Kotlin with Android options * Configure base Kotlin with Android options
@ -36,7 +36,7 @@ internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *, *>, commonExtension: CommonExtension<*, *, *, *, *, *>,
) { ) {
commonExtension.apply { commonExtension.apply {
compileSdk = 34 compileSdk = 35
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
@ -54,7 +54,7 @@ internal fun Project.configureKotlinAndroid(
configureKotlin<KotlinAndroidProjectExtension>() configureKotlin<KotlinAndroidProjectExtension>()
dependencies { dependencies {
add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get()) "coreLibraryDesugaring"(libs.findLibrary("android.desugarJdkLibs").get())
} }
} }
@ -75,7 +75,7 @@ internal fun Project.configureKotlinJvm() {
/** /**
* Configure base Kotlin options * Configure base Kotlin options
*/ */
private inline fun <reified T : KotlinTopLevelExtension> Project.configureKotlin() = configure<T> { private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() = configure<T> {
// Treat all Kotlin warnings as errors (disabled by default) // Treat all Kotlin warnings as errors (disabled by default)
// Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
val warningsAsErrors: String? by project val warningsAsErrors: String? by project
@ -90,5 +90,19 @@ private inline fun <reified T : KotlinTopLevelExtension> Project.configureKotlin
// Enable experimental coroutines APIs, including Flow // Enable experimental coroutines APIs, including Flow
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
) )
freeCompilerArgs.add(
/**
* Remove this args after Phase 3.
* https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-consistent-copy-visibility/#deprecation-timeline
*
* Deprecation timeline
* Phase 3. (Supposedly Kotlin 2.2 or Kotlin 2.3).
* The default changes.
* Unless ExposedCopyVisibility is used, the generated 'copy' method has the same visibility as the primary constructor.
* The binary signature changes. The error on the declaration is no longer reported.
* '-Xconsistent-data-class-copy-visibility' compiler flag and ConsistentCopyVisibility annotation are now unnecessary.
*/
"-Xconsistent-data-class-copy-visibility"
)
} }
} }

@ -16,23 +16,26 @@ enum class FlavorDimension {
@Suppress("EnumEntryName") @Suppress("EnumEntryName")
enum class NiaFlavor(val dimension: FlavorDimension, val applicationIdSuffix: String? = null) { enum class NiaFlavor(val dimension: FlavorDimension, val applicationIdSuffix: String? = null) {
demo(FlavorDimension.contentType, applicationIdSuffix = ".demo"), demo(FlavorDimension.contentType, applicationIdSuffix = ".demo"),
prod(FlavorDimension.contentType) prod(FlavorDimension.contentType),
} }
fun configureFlavors( fun configureFlavors(
commonExtension: CommonExtension<*, *, *, *, *, *>, commonExtension: CommonExtension<*, *, *, *, *, *>,
flavorConfigurationBlock: ProductFlavor.(flavor: NiaFlavor) -> Unit = {} flavorConfigurationBlock: ProductFlavor.(flavor: NiaFlavor) -> Unit = {},
) { ) {
commonExtension.apply { commonExtension.apply {
flavorDimensions += FlavorDimension.contentType.name FlavorDimension.values().forEach { flavorDimension ->
flavorDimensions += flavorDimension.name
}
productFlavors { productFlavors {
NiaFlavor.values().forEach { NiaFlavor.values().forEach { niaFlavor ->
create(it.name) { register(niaFlavor.name) {
dimension = it.dimension.name dimension = niaFlavor.dimension.name
flavorConfigurationBlock(this, it) flavorConfigurationBlock(this, niaFlavor)
if (this@apply is ApplicationExtension && this is ApplicationProductFlavor) { if (this@apply is ApplicationExtension && this is ApplicationProductFlavor) {
if (it.applicationIdSuffix != null) { if (niaFlavor.applicationIdSuffix != null) {
applicationIdSuffix = it.applicationIdSuffix applicationIdSuffix = niaFlavor.applicationIdSuffix
} }
} }
} }

@ -2,3 +2,5 @@
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
org.gradle.configureondemand=true org.gradle.configureondemand=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache.parallel=true

@ -14,9 +14,22 @@
* limitations under the License. * limitations under the License.
*/ */
dependencyResolutionManagement { pluginManagement {
repositories { repositories {
gradlePluginPortal()
google() google()
}
}
dependencyResolutionManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral() mavenCentral()
} }
versionCatalogs { versionCatalogs {

@ -16,10 +16,16 @@
buildscript { buildscript {
repositories { repositories {
google() google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral() mavenCentral()
// Android Build Server // This is used only for internal Google builds.
maven { url = uri("../nowinandroid-prebuilts/m2repository") } maven { url = uri("../nowinandroid-prebuilts/m2repository") }
} }
dependencies { dependencies {

@ -25,7 +25,6 @@ android {
testOptions { testOptions {
unitTests { unitTests {
isIncludeAndroidResources = true isIncludeAndroidResources = true
isReturnDefaultValues = true
} }
} }
} }

@ -89,6 +89,7 @@ class OfflineFirstNewsRepositoryTest {
@Test @Test
fun offlineFirstNewsRepository_news_resources_stream_is_backed_by_news_resource_dao() = fun offlineFirstNewsRepository_news_resources_stream_is_backed_by_news_resource_dao() =
testScope.runTest { testScope.runTest {
subject.syncWith(synchronizer)
assertEquals( assertEquals(
newsResourceDao.getNewsResources() newsResourceDao.getNewsResources()
.first() .first()

@ -67,6 +67,8 @@ class OfflineFirstTopicsRepositoryTest {
@Test @Test
fun offlineFirstTopicsRepository_topics_stream_is_backed_by_topics_dao() = fun offlineFirstTopicsRepository_topics_stream_is_backed_by_topics_dao() =
testScope.runTest { testScope.runTest {
subject.syncWith(synchronizer)
assertEquals( assertEquals(
topicDao.getTopicEntities() topicDao.getTopicEntities()
.first() .first()

@ -0,0 +1,47 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.database.dao
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase
import org.junit.After
import org.junit.Before
internal abstract class DatabaseTest {
private lateinit var db: NiaDatabase
protected lateinit var newsResourceDao: NewsResourceDao
protected lateinit var topicDao: TopicDao
@Before
fun setup() {
db = run {
val context = ApplicationProvider.getApplicationContext<Context>()
Room.inMemoryDatabaseBuilder(
context,
NiaDatabase::class.java,
).build()
}
newsResourceDao = db.newsResourceDao()
topicDao = db.topicDao()
}
@After
fun teardown() = db.close()
}

@ -16,10 +16,6 @@
package com.google.samples.apps.nowinandroid.core.database.dao package com.google.samples.apps.nowinandroid.core.database.dao
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
@ -27,33 +23,13 @@ import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.junit.After
import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class NewsResourceDaoTest { internal class NewsResourceDaoTest : DatabaseTest() {
private lateinit var newsResourceDao: NewsResourceDao
private lateinit var topicDao: TopicDao
private lateinit var db: NiaDatabase
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context,
NiaDatabase::class.java,
).build()
newsResourceDao = db.newsResourceDao()
topicDao = db.topicDao()
}
@After
fun closeDb() = db.close()
@Test @Test
fun newsResourceDao_fetches_items_by_descending_publish_date() = runTest { fun getNewsResources_allEntries_areOrderedByPublishDateDesc() = runTest {
val newsResourceEntities = listOf( val newsResourceEntities = listOf(
testNewsResource( testNewsResource(
id = "0", id = "0",
@ -88,7 +64,7 @@ class NewsResourceDaoTest {
} }
@Test @Test
fun newsResourceDao_filters_items_by_news_ids_by_descending_publish_date() = runTest { fun getNewsResources_filteredById_areOrderedByDescendingPublishDate() = runTest {
val newsResourceEntities = listOf( val newsResourceEntities = listOf(
testNewsResource( testNewsResource(
id = "0", id = "0",
@ -126,7 +102,7 @@ class NewsResourceDaoTest {
} }
@Test @Test
fun newsResourceDao_filters_items_by_topic_ids_by_descending_publish_date() = runTest { fun getNewsResources_filteredByTopicId_areOrderedByDescendingPublishDate() = runTest {
val topicEntities = listOf( val topicEntities = listOf(
testTopicEntity( testTopicEntity(
id = "1", id = "1",
@ -186,7 +162,7 @@ class NewsResourceDaoTest {
} }
@Test @Test
fun newsResourceDao_filters_items_by_news_and_topic_ids_by_descending_publish_date() = runTest { fun getNewsResources_filteredByIdAndTopicId_areOrderedByDescendingPublishDate() = runTest {
val topicEntities = listOf( val topicEntities = listOf(
testTopicEntity( testTopicEntity(
id = "1", id = "1",
@ -248,7 +224,7 @@ class NewsResourceDaoTest {
} }
@Test @Test
fun newsResourceDao_deletes_items_by_ids() = fun deleteNewsResources_byId() =
runTest { runTest {
val newsResourceEntities = listOf( val newsResourceEntities = listOf(
testNewsResource( testNewsResource(

@ -0,0 +1,126 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.database.dao
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertEquals
internal class TopicDaoTest : DatabaseTest() {
@Test
fun getTopics() = runTest {
insertTopics()
val savedTopics = topicDao.getTopicEntities().first()
assertEquals(
listOf("1", "2", "3"),
savedTopics.map { it.id },
)
}
@Test
fun getTopic() = runTest {
insertTopics()
val savedTopicEntity = topicDao.getTopicEntity("2").first()
assertEquals("performance", savedTopicEntity.name)
}
@Test
fun getTopics_oneOff() = runTest {
insertTopics()
val savedTopics = topicDao.getOneOffTopicEntities()
assertEquals(
listOf("1", "2", "3"),
savedTopics.map { it.id },
)
}
@Test
fun getTopics_byId() = runTest {
insertTopics()
val savedTopics = topicDao.getTopicEntities(setOf("1", "2"))
.first()
assertEquals(listOf("compose", "performance"), savedTopics.map { it.name })
}
@Test
fun insertTopic_newEntryIsIgnoredIfAlreadyExists() = runTest {
insertTopics()
topicDao.insertOrIgnoreTopics(
listOf(testTopicEntity("1", "compose")),
)
val savedTopics = topicDao.getOneOffTopicEntities()
assertEquals(3, savedTopics.size)
}
@Test
fun upsertTopic_existingEntryIsUpdated() = runTest {
insertTopics()
topicDao.upsertTopics(
listOf(testTopicEntity("1", "newName")),
)
val savedTopics = topicDao.getOneOffTopicEntities()
assertEquals(3, savedTopics.size)
assertEquals("newName", savedTopics.first().name)
}
@Test
fun deleteTopics_byId_existingEntriesAreDeleted() = runTest {
insertTopics()
topicDao.deleteTopics(listOf("1", "2"))
val savedTopics = topicDao.getOneOffTopicEntities()
assertEquals(1, savedTopics.size)
assertEquals("3", savedTopics.first().id)
}
private suspend fun insertTopics() {
val topicEntities = listOf(
testTopicEntity("1", "compose"),
testTopicEntity("2", "performance"),
testTopicEntity("3", "headline"),
)
topicDao.insertOrIgnoreTopics(topicEntities)
}
}
private fun testTopicEntity(
id: String = "0",
name: String,
) = TopicEntity(
id = id,
name = name,
shortDescription = "",
longDescription = "",
url = "",
imageUrl = "",
)

@ -25,11 +25,6 @@ android {
consumerProguardFiles("consumer-proguard-rules.pro") consumerProguardFiles("consumer-proguard-rules.pro")
} }
namespace = "com.google.samples.apps.nowinandroid.core.datastore" namespace = "com.google.samples.apps.nowinandroid.core.datastore"
testOptions {
unitTests {
isReturnDefaultValues = true
}
}
} }
dependencies { dependencies {

@ -21,9 +21,6 @@ plugins {
} }
android { android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
namespace = "com.google.samples.apps.nowinandroid.core.designsystem" namespace = "com.google.samples.apps.nowinandroid.core.designsystem"
} }
@ -47,6 +44,4 @@ dependencies {
testImplementation(libs.hilt.android.testing) testImplementation(libs.hilt.android.testing)
testImplementation(libs.robolectric) testImplementation(libs.robolectric)
testImplementation(projects.core.screenshotTesting) testImplementation(projects.core.screenshotTesting)
androidTestImplementation(libs.bundles.androidx.compose.ui.test)
} }

@ -28,10 +28,8 @@ import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.NavigationRailItemDefaults import androidx.compose.material3.NavigationRailItemDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
@ -184,10 +182,6 @@ fun NiaNavigationRail(
* @param windowAdaptiveInfo The window adaptive info. * @param windowAdaptiveInfo The window adaptive info.
* @param content The app content inside the scaffold. * @param content The app content inside the scaffold.
*/ */
@OptIn(
ExperimentalMaterial3AdaptiveNavigationSuiteApi::class,
ExperimentalMaterial3AdaptiveApi::class,
)
@Composable @Composable
fun NiaNavigationSuiteScaffold( fun NiaNavigationSuiteScaffold(
navigationSuiteItems: NiaNavigationSuiteScope.() -> Unit, navigationSuiteItems: NiaNavigationSuiteScope.() -> Unit,
@ -242,7 +236,6 @@ fun NiaNavigationSuiteScaffold(
/** /**
* A wrapper around [NavigationSuiteScope] to declare navigation items. * A wrapper around [NavigationSuiteScope] to declare navigation items.
*/ */
@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
class NiaNavigationSuiteScope internal constructor( class NiaNavigationSuiteScope internal constructor(
private val navigationSuiteScope: NavigationSuiteScope, private val navigationSuiteScope: NavigationSuiteScope,
private val navigationSuiteItemColors: NavigationSuiteItemColors, private val navigationSuiteItemColors: NavigationSuiteItemColors,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

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

Loading…
Cancel
Save