Merge branch 'main' into room-gradle-plugin

pull/1055/head
Simon Marquis 7 months ago committed by GitHub
commit 34d8262e00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,26 @@
# 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-compose:
patterns:
- "org.jetbrains.kotlin:*"
- "org.jetbrains.kotlin.jvm"
- "com.google.devtools.ksp"
- "androidx.compose.compiler:compiler"
open-pull-requests-limit: 10
registries:
maven-google:
type: "maven-repository"
url: "https://maven.google.com"
replaces-base: true

@ -1,16 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base", "group:all", ":dependencyDashboard", "schedule:daily"
],
"packageRules": [
{
"matchPackageNames": ["org.objenesis:objenesis"],
"allowedVersions": "<=2.6"
},
{
"matchPackageNames": ["com.google.protobuf"],
"allowedVersions": "<=0.8.19"
}
]
}

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

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

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

@ -69,3 +69,7 @@ dependencies {
implementation(projects.core.ui) implementation(projects.core.ui)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
} }
dependencyGuard {
configuration("releaseRuntimeClasspath")
}

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

@ -144,3 +144,7 @@ baselineProfile {
// Instead enable generation directly for the release build variant. // Instead enable generation directly for the release build variant.
automaticGenerationDuringBuild = false automaticGenerationDuringBuild = false
} }
dependencyGuard {
configuration("prodReleaseRuntimeClasspath")
}

@ -0,0 +1,211 @@
androidx.activity:activity-compose:1.8.0
androidx.activity:activity-ktx:1.8.0
androidx.activity:activity:1.8.0
androidx.annotation:annotation-experimental:1.3.0
androidx.annotation:annotation-jvm:1.6.0
androidx.annotation:annotation:1.6.0
androidx.appcompat:appcompat-resources:1.6.1
androidx.appcompat:appcompat:1.6.1
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0
androidx.browser:browser:1.6.0
androidx.collection:collection-ktx:1.1.0
androidx.collection:collection:1.2.0
androidx.compose.animation:animation-android:1.5.4
androidx.compose.animation:animation-core-android:1.5.4
androidx.compose.animation:animation-core:1.5.4
androidx.compose.animation:animation:1.5.4
androidx.compose.foundation:foundation-android:1.5.4
androidx.compose.foundation:foundation-layout-android:1.5.4
androidx.compose.foundation:foundation-layout:1.5.4
androidx.compose.foundation:foundation:1.5.4
androidx.compose.material3:material3-window-size-class:1.1.2
androidx.compose.material3:material3:1.1.2
androidx.compose.material:material-icons-core-android:1.5.4
androidx.compose.material:material-icons-core:1.5.4
androidx.compose.material:material-icons-extended-android:1.5.4
androidx.compose.material:material-icons-extended:1.5.4
androidx.compose.material:material-ripple-android:1.5.4
androidx.compose.material:material-ripple:1.5.4
androidx.compose.runtime:runtime-android:1.5.4
androidx.compose.runtime:runtime-livedata:1.5.4
androidx.compose.runtime:runtime-saveable-android:1.5.4
androidx.compose.runtime:runtime-saveable:1.5.4
androidx.compose.runtime:runtime-tracing:1.0.0-alpha03
androidx.compose.runtime:runtime:1.5.4
androidx.compose.ui:ui-android:1.5.4
androidx.compose.ui:ui-geometry-android:1.5.4
androidx.compose.ui:ui-geometry:1.5.4
androidx.compose.ui:ui-graphics-android:1.5.4
androidx.compose.ui:ui-graphics:1.5.4
androidx.compose.ui:ui-text-android:1.5.4
androidx.compose.ui:ui-text:1.5.4
androidx.compose.ui:ui-tooling-preview-android:1.5.4
androidx.compose.ui:ui-tooling-preview:1.5.4
androidx.compose.ui:ui-unit-android:1.5.4
androidx.compose.ui:ui-unit:1.5.4
androidx.compose.ui:ui-util-android:1.5.4
androidx.compose.ui:ui-util:1.5.4
androidx.compose.ui:ui:1.5.4
androidx.compose:compose-bom:2023.10.01
androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.12.0
androidx.core:core-splashscreen:1.0.1
androidx.core:core:1.12.0
androidx.cursoradapter:cursoradapter:1.0.0
androidx.customview:customview-poolingcontainer:1.0.0
androidx.customview:customview:1.0.0
androidx.datastore:datastore-core:1.0.0
androidx.datastore:datastore-preferences-core:1.0.0
androidx.datastore:datastore-preferences:1.0.0
androidx.datastore:datastore:1.0.0
androidx.documentfile:documentfile:1.0.0
androidx.drawerlayout:drawerlayout:1.0.0
androidx.emoji2:emoji2-views-helper:1.4.0
androidx.emoji2:emoji2:1.4.0
androidx.exifinterface:exifinterface:1.3.6
androidx.fragment:fragment:1.5.1
androidx.hilt:hilt-common:1.1.0
androidx.hilt:hilt-navigation-compose:1.0.0
androidx.hilt:hilt-navigation:1.0.0
androidx.hilt:hilt-work:1.1.0
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.6.2
androidx.lifecycle:lifecycle-common:2.6.2
androidx.lifecycle:lifecycle-livedata-core-ktx:2.6.2
androidx.lifecycle:lifecycle-livedata-core:2.6.2
androidx.lifecycle:lifecycle-livedata-ktx:2.6.2
androidx.lifecycle:lifecycle-livedata:2.6.2
androidx.lifecycle:lifecycle-process:2.6.2
androidx.lifecycle:lifecycle-runtime-compose:2.6.2
androidx.lifecycle:lifecycle-runtime-ktx:2.6.2
androidx.lifecycle:lifecycle-runtime:2.6.2
androidx.lifecycle:lifecycle-service:2.6.2
androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2
androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2
androidx.lifecycle:lifecycle-viewmodel:2.6.2
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.metrics:metrics-performance:1.0.0-alpha04
androidx.navigation:navigation-common-ktx:2.7.4
androidx.navigation:navigation-common:2.7.4
androidx.navigation:navigation-compose:2.7.4
androidx.navigation:navigation-runtime-ktx:2.7.4
androidx.navigation:navigation-runtime:2.7.4
androidx.print:print:1.0.0
androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05
androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05
androidx.profileinstaller:profileinstaller:1.3.1
androidx.resourceinspection:resourceinspection-annotation:1.0.1
androidx.room:room-common:2.6.0
androidx.room:room-ktx:2.6.0
androidx.room:room-runtime:2.6.0
androidx.savedstate:savedstate-ktx:1.2.1
androidx.savedstate:savedstate:1.2.1
androidx.sqlite:sqlite-framework:2.4.0
androidx.sqlite:sqlite:2.4.0
androidx.startup:startup-runtime:1.1.1
androidx.tracing:tracing-ktx:1.2.0-alpha02
androidx.tracing:tracing-perfetto-common:1.0.0-alpha11
androidx.tracing:tracing-perfetto:1.0.0-alpha11
androidx.tracing:tracing:1.2.0-alpha02
androidx.vectordrawable:vectordrawable-animated:1.1.0
androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager:viewpager:1.0.0
androidx.window.extensions.core:core:1.0.0
androidx.window:window:1.1.0
androidx.work:work-runtime-ktx:2.9.0-rc01
androidx.work:work-runtime:2.9.0-rc01
com.caverock:androidsvg-aar:1.4
com.google.accompanist:accompanist-drawablepainter:0.30.1
com.google.accompanist:accompanist-permissions:0.32.0
com.google.android.datatransport:transport-api:3.0.0
com.google.android.datatransport:transport-backend-cct:3.1.9
com.google.android.datatransport:transport-runtime:3.1.9
com.google.android.gms:play-services-ads-identifier:18.0.0
com.google.android.gms:play-services-base:18.0.1
com.google.android.gms:play-services-basement:18.1.0
com.google.android.gms:play-services-cloud-messaging:17.0.1
com.google.android.gms:play-services-measurement-api:21.4.0
com.google.android.gms:play-services-measurement-base:21.4.0
com.google.android.gms:play-services-measurement-impl:21.4.0
com.google.android.gms:play-services-measurement-sdk-api:21.4.0
com.google.android.gms:play-services-measurement-sdk:21.4.0
com.google.android.gms:play-services-measurement:21.4.0
com.google.android.gms:play-services-oss-licenses:17.0.1
com.google.android.gms:play-services-stats:17.0.2
com.google.android.gms:play-services-tasks:18.0.2
com.google.code.findbugs:jsr305:3.0.2
com.google.dagger:dagger-lint-aar:2.48.1
com.google.dagger:dagger:2.48.1
com.google.dagger:hilt-android:2.48.1
com.google.dagger:hilt-core:2.48.1
com.google.errorprone:error_prone_annotations:2.11.0
com.google.firebase:firebase-abt:21.1.1
com.google.firebase:firebase-analytics-ktx:21.4.0
com.google.firebase:firebase-analytics:21.4.0
com.google.firebase:firebase-annotations:16.2.0
com.google.firebase:firebase-bom:32.4.0
com.google.firebase:firebase-common-ktx:20.4.2
com.google.firebase:firebase-common:20.4.2
com.google.firebase:firebase-components:17.1.5
com.google.firebase:firebase-config:21.5.0
com.google.firebase:firebase-crashlytics-ktx:18.5.0
com.google.firebase:firebase-crashlytics:18.5.0
com.google.firebase:firebase-datatransport:18.1.8
com.google.firebase:firebase-encoders-json:18.0.1
com.google.firebase:firebase-encoders-proto:16.0.0
com.google.firebase:firebase-encoders:17.0.0
com.google.firebase:firebase-iid-interop:17.1.0
com.google.firebase:firebase-installations-interop:17.1.1
com.google.firebase:firebase-installations:17.2.0
com.google.firebase:firebase-measurement-connector:19.0.0
com.google.firebase:firebase-messaging-ktx:23.3.0
com.google.firebase:firebase-messaging:23.3.0
com.google.firebase:firebase-perf-ktx:20.5.0
com.google.firebase:firebase-perf:20.5.0
com.google.firebase:firebase-sessions:1.1.0
com.google.firebase:protolite-well-known-types:18.0.0
com.google.guava:failureaccess:1.0.1
com.google.guava:guava:31.1-android
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
com.google.j2objc:j2objc-annotations:1.3
com.google.protobuf:protobuf-javalite:3.24.4
com.google.protobuf:protobuf-kotlin-lite:3.24.4
com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0
com.squareup.okhttp3:logging-interceptor:4.12.0
com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.6.0
com.squareup.okio:okio:3.6.0
com.squareup.retrofit2:retrofit:2.9.0
io.coil-kt:coil-base:2.4.0
io.coil-kt:coil-compose-base:2.4.0
io.coil-kt:coil-compose:2.4.0
io.coil-kt:coil-svg:2.4.0
io.coil-kt:coil:2.4.0
io.github.aakira:napier-android:1.4.1
io.github.aakira:napier:1.4.1
javax.inject:javax.inject:1
org.checkerframework:checker-qual:3.12.0
org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10
org.jetbrains.kotlin:kotlin-stdlib:1.9.10
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.4.1
org.jetbrains.kotlinx:kotlinx-datetime:0.4.1
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.0
org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.0
org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0
org.jetbrains:annotations:23.0.0

@ -65,7 +65,7 @@ import javax.inject.Inject
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
// Configure Robolectric to use a very large screen size that can fit all of the test sizes. // Configure Robolectric to use a very large screen size that can fit all of the test sizes.
// This allows enough room to render the content under test without clipping or scaling. // This allows enough room to render the content under test without clipping or scaling.
@Config(application = HiltTestApplication::class, qualifiers = "w1000dp-h1000dp-480dpi", sdk = [33]) @Config(application = HiltTestApplication::class, qualifiers = "w1000dp-h1000dp-480dpi")
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
@HiltAndroidTest @HiltAndroidTest
class NiaAppScreenSizesScreenshotTests { class NiaAppScreenSizesScreenshotTests {

@ -42,6 +42,14 @@ dependencies {
compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin) compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.room.gradlePlugin) compileOnly(libs.room.gradlePlugin)
implementation(libs.truth)
}
tasks {
validatePlugins {
enableStricterValidation = true
failOnWarning = true
}
} }
gradlePlugin { gradlePlugin {

@ -15,10 +15,10 @@
*/ */
import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationExtension
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.gradle.BaseExtension import com.android.build.gradle.BaseExtension
import com.google.samples.apps.nowinandroid.configureBadgingTasks import com.google.samples.apps.nowinandroid.configureBadgingTasks
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid 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
@ -33,6 +33,7 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
apply("com.android.application") apply("com.android.application")
apply("org.jetbrains.kotlin.android") apply("org.jetbrains.kotlin.android")
apply("nowinandroid.android.lint") apply("nowinandroid.android.lint")
apply("com.dropbox.dependency-guard")
} }
extensions.configure<ApplicationExtension> { extensions.configure<ApplicationExtension> {
@ -47,4 +48,4 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
} }
} }
} }

@ -20,34 +20,39 @@ import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.gradle.BaseExtension import com.android.build.gradle.BaseExtension
import com.android.SdkConstants import com.android.SdkConstants
import com.google.common.truth.Truth.assertWithMessage
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Copy import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.configurationcache.extensions.capitalized import org.gradle.configurationcache.extensions.capitalized
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.io.File
import java.nio.file.Files
import javax.inject.Inject import javax.inject.Inject
@CacheableTask
abstract class GenerateBadgingTask : DefaultTask() { abstract class GenerateBadgingTask : DefaultTask() {
@get:OutputFile @get:OutputFile
abstract val badging: RegularFileProperty abstract val badging: RegularFileProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile @get:InputFile
abstract val apk: RegularFileProperty abstract val apk: RegularFileProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile @get:InputFile
abstract val aapt2Executable: RegularFileProperty abstract val aapt2Executable: RegularFileProperty
@ -68,6 +73,7 @@ abstract class GenerateBadgingTask : DefaultTask() {
} }
} }
@CacheableTask
abstract class CheckBadgingTask : DefaultTask() { abstract class CheckBadgingTask : DefaultTask() {
// In order for the task to be up-to-date when the inputs have not changed, // In order for the task to be up-to-date when the inputs have not changed,
@ -76,9 +82,11 @@ abstract class CheckBadgingTask : DefaultTask() {
@get:OutputDirectory @get:OutputDirectory
abstract val output: DirectoryProperty abstract val output: DirectoryProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile @get:InputFile
abstract val goldenBadging: RegularFileProperty abstract val goldenBadging: RegularFileProperty
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile @get:InputFile
abstract val generatedBadging: RegularFileProperty abstract val generatedBadging: RegularFileProperty
@ -89,17 +97,12 @@ abstract class CheckBadgingTask : DefaultTask() {
@TaskAction @TaskAction
fun taskAction() { fun taskAction() {
if ( assertWithMessage(
Files.mismatch( "Generated badging is different from golden badging! " +
goldenBadging.get().asFile.toPath(), "If this change is intended, run ./gradlew ${updateBadgingTaskName.get()}",
generatedBadging.get().asFile.toPath(), )
) != -1L .that(generatedBadging.get().asFile.readText())
) { .isEqualTo(goldenBadging.get().asFile.readText())
throw GradleException(
"Generated badging is different from golden badging! " +
"If this change is intended, run ./gradlew ${updateBadgingTaskName.get()}",
)
}
} }
} }

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

@ -30,7 +30,10 @@ import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.work.DisableCachingByDefault
import java.io.File import java.io.File
internal fun Project.configurePrintApksTask(extension: AndroidComponentsExtension<*, *, *>) { internal fun Project.configurePrintApksTask(extension: AndroidComponentsExtension<*, *, *>) {
@ -62,10 +65,14 @@ internal fun Project.configurePrintApksTask(extension: AndroidComponentsExtensio
} }
} }
@DisableCachingByDefault(because = "Prints output")
internal abstract class PrintApkLocationTask : DefaultTask() { internal abstract class PrintApkLocationTask : DefaultTask() {
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputDirectory @get:InputDirectory
abstract val apkFolder: DirectoryProperty abstract val apkFolder: DirectoryProperty
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles @get:InputFiles
abstract val sources: ListProperty<Directory> abstract val sources: ListProperty<Directory>

@ -36,6 +36,7 @@ plugins {
alias(libs.plugins.baselineprofile) apply false alias(libs.plugins.baselineprofile) apply false
alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.dependencyGuard) apply false
alias(libs.plugins.firebase.crashlytics) apply false alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.firebase.perf) apply false alias(libs.plugins.firebase.perf) apply false
alias(libs.plugins.gms) apply false alias(libs.plugins.gms) apply false

@ -16,10 +16,10 @@
package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar
import android.annotation.SuppressLint
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.Orientation.Horizontal import androidx.compose.foundation.gestures.Orientation.Horizontal
import androidx.compose.foundation.gestures.Orientation.Vertical import androidx.compose.foundation.gestures.Orientation.Vertical
@ -38,12 +38,22 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.invalidateDraw
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Active import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Active
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Dormant import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Dormant
@ -130,12 +140,7 @@ private fun ScrollableState.DraggableScrollbarThumb(
Horizontal -> height(12.dp).fillMaxWidth() Horizontal -> height(12.dp).fillMaxWidth()
} }
} }
.background( .scrollThumb(this, interactionSource),
color = scrollbarThumbColor(
interactionSource = interactionSource,
),
shape = RoundedCornerShape(16.dp),
),
) )
} }
@ -155,31 +160,72 @@ private fun ScrollableState.DecorativeScrollbarThumb(
Horizontal -> height(2.dp).fillMaxWidth() Horizontal -> height(2.dp).fillMaxWidth()
} }
} }
.background( .scrollThumb(this, interactionSource),
color = scrollbarThumbColor(
interactionSource = interactionSource,
),
shape = RoundedCornerShape(16.dp),
),
) )
} }
// TODO: This lint is removed in 1.6 as the recommendation has changed
// remove when project is upgraded
@SuppressLint("ComposableModifierFactory")
@Composable
private fun Modifier.scrollThumb(
scrollableState: ScrollableState,
interactionSource: InteractionSource,
): Modifier {
val colorState = scrollbarThumbColor(scrollableState, interactionSource)
return this then ScrollThumbElement { colorState.value }
}
private data class ScrollThumbElement(val colorProducer: ColorProducer) :
ModifierNodeElement<ScrollThumbNode>() {
override fun create(): ScrollThumbNode = ScrollThumbNode(colorProducer)
override fun update(node: ScrollThumbNode) {
node.colorProducer = colorProducer
node.invalidateDraw()
}
}
private class ScrollThumbNode(var colorProducer: ColorProducer) : DrawModifierNode, Modifier.Node() {
private val shape = RoundedCornerShape(16.dp)
// naive cache outline calculation if size is the same
private var lastSize: Size? = null
private var lastLayoutDirection: LayoutDirection? = null
private var lastOutline: Outline? = null
override fun ContentDrawScope.draw() {
val color = colorProducer()
val outline =
if (size == lastSize && layoutDirection == lastLayoutDirection) {
lastOutline!!
} else {
shape.createOutline(size, layoutDirection, this)
}
if (color != Color.Unspecified) drawOutline(outline, color = color)
lastOutline = outline
lastSize = size
lastLayoutDirection = layoutDirection
}
}
/** /**
* The color of the scrollbar thumb as a function of its interaction state. * The color of the scrollbar thumb as a function of its interaction state.
* @param interactionSource source of interactions in the scrolling container * @param interactionSource source of interactions in the scrolling container
*/ */
@Composable @Composable
private fun ScrollableState.scrollbarThumbColor( private fun scrollbarThumbColor(
scrollableState: ScrollableState,
interactionSource: InteractionSource, interactionSource: InteractionSource,
): Color { ): State<Color> {
var state by remember { mutableStateOf(Dormant) } var state by remember { mutableStateOf(Dormant) }
val pressed by interactionSource.collectIsPressedAsState() val pressed by interactionSource.collectIsPressedAsState()
val hovered by interactionSource.collectIsHoveredAsState() val hovered by interactionSource.collectIsHoveredAsState()
val dragged by interactionSource.collectIsDraggedAsState() val dragged by interactionSource.collectIsDraggedAsState()
val active = (canScrollForward || canScrollForward) && val active = (scrollableState.canScrollForward || scrollableState.canScrollBackward) &&
(pressed || hovered || dragged || isScrollInProgress) (pressed || hovered || dragged || scrollableState.isScrollInProgress)
val color by animateColorAsState( val color = animateColorAsState(
targetValue = when (state) { targetValue = when (state) {
Active -> MaterialTheme.colorScheme.onSurface.copy(0.5f) Active -> MaterialTheme.colorScheme.onSurface.copy(0.5f)
Inactive -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f) Inactive -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)

@ -16,8 +16,9 @@
package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar package com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.Orientation.Horizontal
import androidx.compose.foundation.gestures.Orientation.Vertical
import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.gestures.detectVerticalDragGestures
@ -28,31 +29,28 @@ import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.util.packFloats import androidx.compose.ui.util.packFloats
import androidx.compose.ui.util.unpackFloat1 import androidx.compose.ui.util.unpackFloat1
import androidx.compose.ui.util.unpackFloat2 import androidx.compose.ui.util.unpackFloat2
@ -61,6 +59,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt
/** /**
* The delay between scrolls when a user long presses on the scrollbar track to initiate a scroll * The delay between scrolls when a user long presses on the scrollbar track to initiate a scroll
@ -74,21 +73,59 @@ private const val SCROLLBAR_PRESS_DELAY_MS = 10L
*/ */
private const val SCROLLBAR_PRESS_DELTA_PCT = 0.02f private const val SCROLLBAR_PRESS_DELTA_PCT = 0.02f
class ScrollbarState {
private var packedValue by mutableLongStateOf(0L)
internal fun onScroll(stateValue: ScrollbarStateValue) {
packedValue = stateValue.packedValue
}
/**
* Returns the thumb size of the scrollbar as a percentage of the total track size
*/
val thumbSizePercent
get() = unpackFloat1(packedValue)
/**
* Returns the distance the thumb has traveled as a percentage of total track size
*/
val thumbMovedPercent
get() = unpackFloat2(packedValue)
/**
* Returns the max distance the thumb can travel as a percentage of total track size
*/
val thumbTrackSizePercent
get() = 1f - thumbSizePercent
}
/**
* Returns the size of the scrollbar track in pixels
*/
private val ScrollbarTrack.size
get() = unpackFloat2(packedValue) - unpackFloat1(packedValue)
/**
* Returns the position of the scrollbar thumb on the track as a percentage
*/
private fun ScrollbarTrack.thumbPosition(
dimension: Float,
): Float = max(
a = min(
a = dimension / size,
b = 1f,
),
b = 0f,
)
/** /**
* Class definition for the core properties of a scroll bar * Class definition for the core properties of a scroll bar
*/ */
@Immutable @Immutable
@JvmInline @JvmInline
value class ScrollbarState internal constructor( value class ScrollbarStateValue internal constructor(
internal val packedValue: Long, internal val packedValue: Long,
) { )
companion object {
val FULL = ScrollbarState(
thumbSizePercent = 1f,
thumbMovedPercent = 0f,
)
}
}
/** /**
* Class definition for the core properties of a scroll bar track * Class definition for the core properties of a scroll bar track
@ -105,54 +142,23 @@ private value class ScrollbarTrack(
} }
/** /**
* Creates a [ScrollbarState] with the listed properties * Creates a [ScrollbarStateValue] with the listed properties
* @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size. * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size.
* Refers to either the thumb width (for horizontal scrollbars) * Refers to either the thumb width (for horizontal scrollbars)
* or height (for vertical scrollbars). * or height (for vertical scrollbars).
* @param thumbMovedPercent the distance the thumb has traveled as a percentage of total * @param thumbMovedPercent the distance the thumb has traveled as a percentage of total
* track size. * track size.
*/ */
fun ScrollbarState( fun scrollbarStateValue(
thumbSizePercent: Float, thumbSizePercent: Float,
thumbMovedPercent: Float, thumbMovedPercent: Float,
) = ScrollbarState( ) = ScrollbarStateValue(
packFloats( packFloats(
val1 = thumbSizePercent, val1 = thumbSizePercent,
val2 = thumbMovedPercent, val2 = thumbMovedPercent,
), ),
) )
/**
* Returns the thumb size of the scrollbar as a percentage of the total track size
*/
val ScrollbarState.thumbSizePercent
get() = unpackFloat1(packedValue)
/**
* Returns the distance the thumb has traveled as a percentage of total track size
*/
val ScrollbarState.thumbMovedPercent
get() = unpackFloat2(packedValue)
/**
* Returns the size of the scrollbar track in pixels
*/
private val ScrollbarTrack.size
get() = unpackFloat2(packedValue) - unpackFloat1(packedValue)
/**
* Returns the position of the scrollbar thumb on the track as a percentage
*/
private fun ScrollbarTrack.thumbPosition(
dimension: Float,
): Float = max(
a = min(
a = dimension / size,
b = 1f,
),
b = 0f,
)
/** /**
* Returns the value of [offset] along the axis specified by [this] * Returns the value of [offset] along the axis specified by [this]
*/ */
@ -197,8 +203,6 @@ fun Scrollbar(
thumb: @Composable () -> Unit, thumb: @Composable () -> Unit,
onThumbMoved: ((Float) -> Unit)? = null, onThumbMoved: ((Float) -> Unit)? = null,
) { ) {
val localDensity = LocalDensity.current
// Using Offset.Unspecified and Float.NaN instead of null // Using Offset.Unspecified and Float.NaN instead of null
// to prevent unnecessary boxing of primitives // to prevent unnecessary boxing of primitives
var pressedOffset by remember { mutableStateOf(Offset.Unspecified) } var pressedOffset by remember { mutableStateOf(Offset.Unspecified) }
@ -210,23 +214,6 @@ fun Scrollbar(
var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) } var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) }
val thumbTravelPercent = when {
interactionThumbTravelPercent.isNaN() -> state.thumbMovedPercent
else -> interactionThumbTravelPercent
}
val thumbSizePx = max(
a = state.thumbSizePercent * track.size,
b = with(localDensity) { minThumbSize.toPx() },
)
val thumbSizeDp by animateDpAsState(
targetValue = with(localDensity) { thumbSizePx.toDp() },
label = "scrollbar thumb size",
)
val thumbMovedPx = min(
a = track.size * thumbTravelPercent,
b = track.size - thumbSizePx,
)
// scrollbar track container // scrollbar track container
Box( Box(
modifier = modifier modifier = modifier
@ -320,84 +307,113 @@ fun Scrollbar(
} }
}, },
) { ) {
val scrollbarThumbMovedDp = max(
a = with(localDensity) { thumbMovedPx.toDp() },
b = 0.dp,
)
// scrollbar thumb container // scrollbar thumb container
Box( Layout(content = { thumb() }) { measurables, constraints ->
modifier = Modifier val measurable = measurables.first()
.align(Alignment.TopStart)
.run { val thumbSizePx = max(
when (orientation) { a = state.thumbSizePercent * track.size,
Orientation.Horizontal -> width(thumbSizeDp) b = minThumbSize.toPx(),
Orientation.Vertical -> height(thumbSizeDp) )
}
} val trackSizePx = when (state.thumbTrackSizePercent) {
.offset( 0f -> track.size
y = when (orientation) { else -> (track.size - thumbSizePx) / state.thumbTrackSizePercent
Orientation.Horizontal -> 0.dp }
Orientation.Vertical -> scrollbarThumbMovedDp
}, val thumbTravelPercent = max(
x = when (orientation) { a = min(
Orientation.Horizontal -> scrollbarThumbMovedDp a = when {
Orientation.Vertical -> 0.dp interactionThumbTravelPercent.isNaN() -> state.thumbMovedPercent
else -> interactionThumbTravelPercent
}, },
b = state.thumbTrackSizePercent,
), ),
) { b = 0f,
thumb() )
val thumbMovedPx = trackSizePx * thumbTravelPercent
val y = when (orientation) {
Horizontal -> 0
Vertical -> thumbMovedPx.roundToInt()
}
val x = when (orientation) {
Horizontal -> thumbMovedPx.roundToInt()
Vertical -> 0
}
val updatedConstraints = when (orientation) {
Horizontal -> {
constraints.copy(
minWidth = thumbSizePx.roundToInt(),
maxWidth = thumbSizePx.roundToInt(),
)
}
Vertical -> {
constraints.copy(
minHeight = thumbSizePx.roundToInt(),
maxHeight = thumbSizePx.roundToInt(),
)
}
}
val placeable = measurable.measure(updatedConstraints)
layout(placeable.width, placeable.height) {
placeable.place(x, y)
}
} }
} }
if (onThumbMoved == null) return if (onThumbMoved == null) return
// State that will be read inside the effects that follow
// but will not cause re-triggering of them
val updatedState by rememberUpdatedState(state)
// Process presses // Process presses
LaunchedEffect(pressedOffset) { LaunchedEffect(Unit) {
// Press ended, reset interactionThumbTravelPercent snapshotFlow { pressedOffset }.collect { pressedOffset ->
if (pressedOffset == Offset.Unspecified) { // Press ended, reset interactionThumbTravelPercent
interactionThumbTravelPercent = Float.NaN if (pressedOffset == Offset.Unspecified) {
return@LaunchedEffect interactionThumbTravelPercent = Float.NaN
} return@collect
}
var currentThumbMovedPercent = updatedState.thumbMovedPercent var currentThumbMovedPercent = state.thumbMovedPercent
val destinationThumbMovedPercent = track.thumbPosition( val destinationThumbMovedPercent = track.thumbPosition(
dimension = orientation.valueOf(pressedOffset), dimension = orientation.valueOf(pressedOffset),
) )
val isPositive = currentThumbMovedPercent < destinationThumbMovedPercent val isPositive = currentThumbMovedPercent < destinationThumbMovedPercent
val delta = SCROLLBAR_PRESS_DELTA_PCT * if (isPositive) 1f else -1f val delta = SCROLLBAR_PRESS_DELTA_PCT * if (isPositive) 1f else -1f
while (currentThumbMovedPercent != destinationThumbMovedPercent) { while (currentThumbMovedPercent != destinationThumbMovedPercent) {
currentThumbMovedPercent = when { currentThumbMovedPercent = when {
isPositive -> min( isPositive -> min(
a = currentThumbMovedPercent + delta, a = currentThumbMovedPercent + delta,
b = destinationThumbMovedPercent, b = destinationThumbMovedPercent,
) )
else -> max( else -> max(
a = currentThumbMovedPercent + delta, a = currentThumbMovedPercent + delta,
b = destinationThumbMovedPercent, b = destinationThumbMovedPercent,
) )
}
onThumbMoved(currentThumbMovedPercent)
interactionThumbTravelPercent = currentThumbMovedPercent
delay(SCROLLBAR_PRESS_DELAY_MS)
} }
onThumbMoved(currentThumbMovedPercent)
interactionThumbTravelPercent = currentThumbMovedPercent
delay(SCROLLBAR_PRESS_DELAY_MS)
} }
} }
// Process drags // Process drags
LaunchedEffect(draggedOffset) { LaunchedEffect(Unit) {
if (draggedOffset == Offset.Unspecified) { snapshotFlow { draggedOffset }.collect { draggedOffset ->
interactionThumbTravelPercent = Float.NaN if (draggedOffset == Offset.Unspecified) {
return@LaunchedEffect interactionThumbTravelPercent = Float.NaN
return@collect
}
val currentTravel = track.thumbPosition(
dimension = orientation.valueOf(draggedOffset),
)
onThumbMoved(currentTravel)
interactionThumbTravelPercent = currentTravel
} }
val currentTravel = track.thumbPosition(
dimension = orientation.valueOf(draggedOffset),
)
onThumbMoved(currentTravel)
interactionThumbTravelPercent = currentTravel
} }
} }

@ -24,7 +24,8 @@ import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.produceState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
@ -40,58 +41,58 @@ import kotlin.math.min
fun LazyListState.scrollbarState( fun LazyListState.scrollbarState(
itemsAvailable: Int, itemsAvailable: Int,
itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index, itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index,
): ScrollbarState = produceState( ): ScrollbarState {
initialValue = ScrollbarState.FULL, val state = remember { ScrollbarState() }
key1 = this, LaunchedEffect(this, itemsAvailable) {
key2 = itemsAvailable, snapshotFlow {
) { if (itemsAvailable == 0) return@snapshotFlow null
snapshotFlow {
if (itemsAvailable == 0) return@snapshotFlow null val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null val firstIndex = min(
a = interpolateFirstItemIndex(
val firstIndex = min( visibleItems = visibleItemsInfo,
a = interpolateFirstItemIndex( itemSize = { it.size },
visibleItems = visibleItemsInfo, offset = { it.offset },
itemSize = { it.size }, nextItemOnMainAxis = { first -> visibleItemsInfo.find { it != first } },
offset = { it.offset }, itemIndex = itemIndex,
nextItemOnMainAxis = { first -> visibleItemsInfo.find { it != first } }, ),
itemIndex = itemIndex, b = itemsAvailable.toFloat(),
), )
b = itemsAvailable.toFloat(), if (firstIndex.isNaN()) return@snapshotFlow null
)
if (firstIndex.isNaN()) return@snapshotFlow null val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
itemVisibilityPercentage(
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo -> itemSize = itemInfo.size,
itemVisibilityPercentage( itemStartOffset = itemInfo.offset,
itemSize = itemInfo.size, viewportStartOffset = layoutInfo.viewportStartOffset,
itemStartOffset = itemInfo.offset, viewportEndOffset = layoutInfo.viewportEndOffset,
viewportStartOffset = layoutInfo.viewportStartOffset, )
viewportEndOffset = layoutInfo.viewportEndOffset, }
val thumbTravelPercent = min(
a = firstIndex / itemsAvailable,
b = 1f,
)
val thumbSizePercent = min(
a = itemsVisible / itemsAvailable,
b = 1f,
)
scrollbarStateValue(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when {
layoutInfo.reverseLayout -> 1f - thumbTravelPercent
else -> thumbTravelPercent
},
) )
} }
.filterNotNull()
val thumbTravelPercent = min( .distinctUntilChanged()
a = firstIndex / itemsAvailable, .collect { state.onScroll(it) }
b = 1f,
)
val thumbSizePercent = min(
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when {
layoutInfo.reverseLayout -> 1f - thumbTravelPercent
else -> thumbTravelPercent
},
)
} }
.filterNotNull() return state
.distinctUntilChanged() }
.collect { value = it }
}.value
/** /**
* Calculates a [ScrollbarState] driven by the changes in a [LazyGridState] * Calculates a [ScrollbarState] driven by the changes in a [LazyGridState]
@ -103,68 +104,68 @@ fun LazyListState.scrollbarState(
fun LazyGridState.scrollbarState( fun LazyGridState.scrollbarState(
itemsAvailable: Int, itemsAvailable: Int,
itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index, itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index,
): ScrollbarState = produceState( ): ScrollbarState {
initialValue = ScrollbarState.FULL, val state = remember { ScrollbarState() }
key1 = this, LaunchedEffect(this, itemsAvailable) {
key2 = itemsAvailable, snapshotFlow {
) { if (itemsAvailable == 0) return@snapshotFlow null
snapshotFlow {
if (itemsAvailable == 0) return@snapshotFlow null val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null val firstIndex = min(
a = interpolateFirstItemIndex(
val firstIndex = min( visibleItems = visibleItemsInfo,
a = interpolateFirstItemIndex( itemSize = { layoutInfo.orientation.valueOf(it.size) },
visibleItems = visibleItemsInfo, offset = { layoutInfo.orientation.valueOf(it.offset) },
itemSize = { layoutInfo.orientation.valueOf(it.size) }, nextItemOnMainAxis = { first ->
offset = { layoutInfo.orientation.valueOf(it.offset) }, when (layoutInfo.orientation) {
nextItemOnMainAxis = { first -> Orientation.Vertical -> visibleItemsInfo.find {
when (layoutInfo.orientation) { it != first && it.row != first.row
Orientation.Vertical -> visibleItemsInfo.find { }
it != first && it.row != first.row
} Orientation.Horizontal -> visibleItemsInfo.find {
it != first && it.column != first.column
Orientation.Horizontal -> visibleItemsInfo.find { }
it != first && it.column != first.column
} }
} },
itemIndex = itemIndex,
),
b = itemsAvailable.toFloat(),
)
if (firstIndex.isNaN()) return@snapshotFlow null
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
itemVisibilityPercentage(
itemSize = layoutInfo.orientation.valueOf(itemInfo.size),
itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset),
viewportStartOffset = layoutInfo.viewportStartOffset,
viewportEndOffset = layoutInfo.viewportEndOffset,
)
}
val thumbTravelPercent = min(
a = firstIndex / itemsAvailable,
b = 1f,
)
val thumbSizePercent = min(
a = itemsVisible / itemsAvailable,
b = 1f,
)
scrollbarStateValue(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when {
layoutInfo.reverseLayout -> 1f - thumbTravelPercent
else -> thumbTravelPercent
}, },
itemIndex = itemIndex,
),
b = itemsAvailable.toFloat(),
)
if (firstIndex.isNaN()) return@snapshotFlow null
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
itemVisibilityPercentage(
itemSize = layoutInfo.orientation.valueOf(itemInfo.size),
itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset),
viewportStartOffset = layoutInfo.viewportStartOffset,
viewportEndOffset = layoutInfo.viewportEndOffset,
) )
} }
.filterNotNull()
val thumbTravelPercent = min( .distinctUntilChanged()
a = firstIndex / itemsAvailable, .collect { state.onScroll(it) }
b = 1f,
)
val thumbSizePercent = min(
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when {
layoutInfo.reverseLayout -> 1f - thumbTravelPercent
else -> thumbTravelPercent
},
)
} }
.filterNotNull() return state
.distinctUntilChanged() }
.collect { value = it }
}.value
/** /**
* Remembers a [ScrollbarState] driven by the changes in a [LazyStaggeredGridState] * Remembers a [ScrollbarState] driven by the changes in a [LazyStaggeredGridState]
@ -177,57 +178,57 @@ fun LazyGridState.scrollbarState(
fun LazyStaggeredGridState.scrollbarState( fun LazyStaggeredGridState.scrollbarState(
itemsAvailable: Int, itemsAvailable: Int,
itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index, itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index,
): ScrollbarState = produceState( ): ScrollbarState {
initialValue = ScrollbarState.FULL, val state = remember { ScrollbarState() }
key1 = this, LaunchedEffect(this, itemsAvailable) {
key2 = itemsAvailable, snapshotFlow {
) { if (itemsAvailable == 0) return@snapshotFlow null
snapshotFlow {
if (itemsAvailable == 0) return@snapshotFlow null val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null val firstIndex = min(
a = interpolateFirstItemIndex(
val firstIndex = min( visibleItems = visibleItemsInfo,
a = interpolateFirstItemIndex( itemSize = { layoutInfo.orientation.valueOf(it.size) },
visibleItems = visibleItemsInfo, offset = { layoutInfo.orientation.valueOf(it.offset) },
itemSize = { layoutInfo.orientation.valueOf(it.size) }, nextItemOnMainAxis = { first ->
offset = { layoutInfo.orientation.valueOf(it.offset) }, visibleItemsInfo.find { it != first && it.lane == first.lane }
nextItemOnMainAxis = { first -> },
visibleItemsInfo.find { it != first && it.lane == first.lane } itemIndex = itemIndex,
}, ),
itemIndex = itemIndex, b = itemsAvailable.toFloat(),
), )
b = itemsAvailable.toFloat(), if (firstIndex.isNaN()) return@snapshotFlow null
)
if (firstIndex.isNaN()) return@snapshotFlow null val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
itemVisibilityPercentage(
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo -> itemSize = layoutInfo.orientation.valueOf(itemInfo.size),
itemVisibilityPercentage( itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset),
itemSize = layoutInfo.orientation.valueOf(itemInfo.size), viewportStartOffset = layoutInfo.viewportStartOffset,
itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset), viewportEndOffset = layoutInfo.viewportEndOffset,
viewportStartOffset = layoutInfo.viewportStartOffset, )
viewportEndOffset = layoutInfo.viewportEndOffset, }
val thumbTravelPercent = min(
a = firstIndex / itemsAvailable,
b = 1f,
)
val thumbSizePercent = min(
a = itemsVisible / itemsAvailable,
b = 1f,
)
scrollbarStateValue(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = thumbTravelPercent,
) )
} }
.filterNotNull()
val thumbTravelPercent = min( .distinctUntilChanged()
a = firstIndex / itemsAvailable, .collect { state.onScroll(it) }
b = 1f,
)
val thumbSizePercent = min(
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = thumbTravelPercent,
)
} }
.filterNotNull() return state
.distinctUntilChanged() }
.collect { value = it }
}.value
private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float { private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float {
var sum = 0f var sum = 0f

@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import kotlin.math.roundToInt
/** /**
* Remembers a function to react to [Scrollbar] thumb position displacements for a [LazyListState] * Remembers a function to react to [Scrollbar] thumb position displacements for a [LazyListState]
@ -79,7 +80,7 @@ private inline fun rememberDraggableScroller(
LaunchedEffect(percentage) { LaunchedEffect(percentage) {
if (percentage.isNaN()) return@LaunchedEffect if (percentage.isNaN()) return@LaunchedEffect
val indexToFind = (itemCount * percentage).toInt() val indexToFind = (itemCount * percentage).roundToInt()
scroll(indexToFind) scroll(indexToFind)
} }
return remember { return remember {

@ -36,7 +36,7 @@ import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @Config(application = HiltTestApplication::class, qualifiers = "480dpi")
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class BackgroundScreenshotTests { class BackgroundScreenshotTests {

@ -36,7 +36,7 @@ import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @Config(application = HiltTestApplication::class, qualifiers = "480dpi")
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class ButtonScreenshotTests { class ButtonScreenshotTests {

@ -43,7 +43,7 @@ import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @Config(application = HiltTestApplication::class, qualifiers = "480dpi")
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class FilterChipScreenshotTests() { class FilterChipScreenshotTests() {

@ -35,7 +35,7 @@ import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @Config(application = HiltTestApplication::class, qualifiers = "480dpi")
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class IconButtonScreenshotTests { class IconButtonScreenshotTests {

@ -37,7 +37,7 @@ import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @Config(application = HiltTestApplication::class, qualifiers = "480dpi")
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class LoadingWheelScreenshotTests() { class LoadingWheelScreenshotTests() {

@ -44,7 +44,7 @@ import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @Config(application = HiltTestApplication::class, qualifiers = "480dpi")
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class NavigationScreenshotTests() { class NavigationScreenshotTests() {

@ -42,7 +42,7 @@ import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @Config(application = HiltTestApplication::class, qualifiers = "480dpi")
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class TabsScreenshotTests() { class TabsScreenshotTests() {

@ -39,7 +39,7 @@ import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @Config(application = HiltTestApplication::class, qualifiers = "480dpi")
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class TagScreenshotTests() { class TagScreenshotTests() {

@ -43,7 +43,7 @@ import org.robolectric.annotation.LooperMode
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33], qualifiers = "480dpi") @Config(application = HiltTestApplication::class, qualifiers = "480dpi")
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class TopAppBarScreenshotTests() { class TopAppBarScreenshotTests() {

@ -30,9 +30,6 @@ import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -64,9 +61,6 @@ fun LazyStaggeredGridScope.newsFeed(
key = { it.id }, key = { it.id },
contentType = { "newsFeedItem" }, contentType = { "newsFeedItem" },
) { userNewsResource -> ) { userNewsResource ->
val resourceUrl by remember {
mutableStateOf(Uri.parse(userNewsResource.url))
}
val context = LocalContext.current val context = LocalContext.current
val analyticsHelper = LocalAnalyticsHelper.current val analyticsHelper = LocalAnalyticsHelper.current
val backgroundColor = MaterialTheme.colorScheme.background.toArgb() val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
@ -79,7 +73,8 @@ fun LazyStaggeredGridScope.newsFeed(
analyticsHelper.logNewsResourceOpened( analyticsHelper.logNewsResourceOpened(
newsResourceId = userNewsResource.id, newsResourceId = userNewsResource.id,
) )
launchCustomChromeTab(context, resourceUrl, backgroundColor) launchCustomChromeTab(context, Uri.parse(userNewsResource.url), backgroundColor)
onNewsResourceViewed(userNewsResource.id) onNewsResourceViewed(userNewsResource.id)
}, },
hasBeenViewed = userNewsResource.hasBeenViewed, hasBeenViewed = userNewsResource.hasBeenViewed,

@ -46,7 +46,7 @@ import java.util.TimeZone
*/ */
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(application = HiltTestApplication::class, sdk = [33]) @Config(application = HiltTestApplication::class)
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class ForYouScreenScreenshotTests { class ForYouScreenScreenshotTests {

@ -1,15 +1,15 @@
[versions] [versions]
accompanist = "0.32.0" accompanist = "0.32.0"
androidDesugarJdkLibs = "2.0.3" androidDesugarJdkLibs = "2.0.4"
# AGP and tools should be updated together # AGP and tools should be updated together
androidGradlePlugin = "8.1.3" androidGradlePlugin = "8.2.0"
androidTools = "31.1.3" androidTools = "31.1.3"
androidxActivity = "1.8.0" androidxActivity = "1.8.0"
androidxAppCompat = "1.6.1" androidxAppCompat = "1.6.1"
androidxBrowser = "1.6.0" androidxBrowser = "1.6.0"
androidxComposeBom = "2023.10.01" androidxComposeBom = "2023.10.01"
androidxComposeCompiler = "1.5.3" androidxComposeCompiler = "1.5.7"
androidxComposeRuntimeTracing = "1.0.0-alpha03" androidxComposeRuntimeTracing = "1.0.0-beta01"
androidxCore = "1.12.0" androidxCore = "1.12.0"
androidxCoreSplashscreen = "1.0.1" androidxCoreSplashscreen = "1.0.1"
androidxDataStore = "1.0.0" androidxDataStore = "1.0.0"
@ -17,7 +17,7 @@ androidxEspresso = "3.5.1"
androidxHiltNavigationCompose = "1.0.0" androidxHiltNavigationCompose = "1.0.0"
androidxJunit = "1.1.5" androidxJunit = "1.1.5"
androidxLifecycle = "2.6.2" androidxLifecycle = "2.6.2"
androidxMacroBenchmark = "1.2.0" androidxMacroBenchmark = "1.2.2"
androidxMetrics = "1.0.0-alpha04" androidxMetrics = "1.0.0-alpha04"
androidxNavigation = "2.7.4" androidxNavigation = "2.7.4"
androidxProfileinstaller = "1.3.1" androidxProfileinstaller = "1.3.1"
@ -28,34 +28,36 @@ androidxTestRules = "1.5.0"
androidxTestRunner = "1.5.2" androidxTestRunner = "1.5.2"
androidxTracing = "1.1.0" androidxTracing = "1.1.0"
androidxUiAutomator = "2.2.0" androidxUiAutomator = "2.2.0"
androidxWindowManager = "1.1.0" androidxWindowManager = "1.2.0"
androidxWork = "2.9.0-rc01" androidxWork = "2.9.0-rc01"
coil = "2.4.0" coil = "2.5.0"
dependencyGuard = "0.4.3"
firebaseBom = "32.4.0" firebaseBom = "32.4.0"
firebaseCrashlyticsPlugin = "2.9.9" firebaseCrashlyticsPlugin = "2.9.9"
firebasePerfPlugin = "1.4.2" firebasePerfPlugin = "1.4.2"
gmsPlugin = "4.4.0" gmsPlugin = "4.4.0"
googleOss = "17.0.1" googleOss = "17.0.1"
googleOssPlugin = "0.10.6" googleOssPlugin = "0.10.6"
hilt = "2.48.1" hilt = "2.50"
hiltExt = "1.1.0" hiltExt = "1.1.0"
jacoco = "0.8.7" jacoco = "0.8.7"
junit4 = "4.13.2" junit4 = "4.13.2"
kotlin = "1.9.10" kotlin = "1.9.21"
kotlinxCoroutines = "1.7.3" kotlinxCoroutines = "1.7.3"
kotlinxDatetime = "0.4.1" kotlinxDatetime = "0.5.0"
kotlinxSerializationJson = "1.6.0" kotlinxSerializationJson = "1.6.0"
ksp = "1.9.10-1.0.13" ksp = "1.9.21-1.0.16"
lint = "31.1.3" lint = "31.2.0"
okhttp = "4.12.0" okhttp = "4.12.0"
protobuf = "3.24.4" protobuf = "3.24.4"
protobufPlugin = "0.9.4" protobufPlugin = "0.9.4"
retrofit = "2.9.0" retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "1.0.0" retrofitKotlinxSerializationJson = "1.0.0"
robolectric = "4.10.3" robolectric = "4.11.1"
roborazzi = "1.6.0" roborazzi = "1.6.0"
room = "2.6.0" room = "2.6.1"
secrets = "2.0.1" secrets = "2.0.1"
truth = "1.1.5"
turbine = "1.0.0" turbine = "1.0.0"
[libraries] [libraries]
@ -67,6 +69,7 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
androidx-benchmark-macro = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "androidxMacroBenchmark" } androidx-benchmark-macro = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "androidxMacroBenchmark" }
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" } androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" }
androidx-compose-compiler = { group = "androidx.compose.compiler", name = "compiler", version.ref = "androidxComposeCompiler" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" }
androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" }
@ -138,6 +141,7 @@ roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", versi
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
truth = { group = "com.google.truth", name = "truth", version.ref = "truth" }
turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }
# Dependencies of the included build-logic # Dependencies of the included build-logic
@ -156,6 +160,7 @@ android-application = { id = "com.android.application", version.ref = "androidGr
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
baselineprofile = { id = "androidx.baselineprofile", version.ref = "androidxMacroBenchmark"} baselineprofile = { id = "androidx.baselineprofile", version.ref = "androidxMacroBenchmark"}
dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dependencyGuard" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" }
firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" } firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" }
gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" } gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" }

Loading…
Cancel
Save