diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d77a706b3..433a7e4a3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,12 +12,11 @@ updates: registries: "*" labels: [ "version update" ] groups: - kotlin-ksp-compose: + kotlin-ksp: patterns: - "org.jetbrains.kotlin:*" - "org.jetbrains.kotlin.jvm" - "com.google.devtools.ksp" - - "androidx.compose.compiler:compiler" open-pull-requests-limit: 10 registries: maven-google: diff --git a/.gitignore b/.gitignore index d4482596d..cc7ae83f7 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ _sandbox # Android Studio captures folder captures/ + +# Kotlin +.kotlin diff --git a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt index 686e709ed..b9135ed42 100644 --- a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt +++ b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt @@ -2,8 +2,8 @@ androidx.activity:activity-compose:1.8.2 androidx.activity:activity-ktx:1.8.2 androidx.activity:activity:1.8.2 androidx.annotation:annotation-experimental:1.4.0 -androidx.annotation:annotation-jvm:1.7.1 -androidx.annotation:annotation:1.7.1 +androidx.annotation:annotation-jvm:1.8.0 +androidx.annotation:annotation:1.8.0 androidx.appcompat:appcompat-resources:1.6.1 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 @@ -12,61 +12,72 @@ androidx.browser:browser:1.8.0 androidx.collection:collection-jvm:1.4.0 androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 -androidx.compose.animation:animation-android:1.6.3 -androidx.compose.animation:animation-core-android:1.6.3 -androidx.compose.animation:animation-core:1.6.3 -androidx.compose.animation:animation:1.6.3 -androidx.compose.foundation:foundation-android:1.6.3 -androidx.compose.foundation:foundation-layout-android:1.6.3 -androidx.compose.foundation:foundation-layout:1.6.3 -androidx.compose.foundation:foundation:1.6.3 -androidx.compose.material3:material3-android:1.2.1 -androidx.compose.material3:material3:1.2.1 +androidx.compose.animation:animation-android:1.7.0-beta01 +androidx.compose.animation:animation-core-android:1.7.0-beta01 +androidx.compose.animation:animation-core:1.7.0-beta01 +androidx.compose.animation:animation:1.7.0-beta01 +androidx.compose.foundation:foundation-android:1.7.0-beta01 +androidx.compose.foundation:foundation-layout-android:1.7.0-beta01 +androidx.compose.foundation:foundation-layout:1.7.0-beta01 +androidx.compose.foundation:foundation:1.7.0-beta01 +androidx.compose.material3.adaptive:adaptive-android:1.0.0-beta01 +androidx.compose.material3.adaptive:adaptive:1.0.0-beta01 +androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.0-beta01 +androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0-beta01 +androidx.compose.material3:material3-android:1.3.0-beta01 +androidx.compose.material3:material3:1.3.0-beta01 androidx.compose.material:material-icons-core-android:1.6.3 androidx.compose.material:material-icons-core:1.6.3 androidx.compose.material:material-icons-extended-android:1.6.3 androidx.compose.material:material-icons-extended:1.6.3 -androidx.compose.material:material-ripple-android:1.6.3 -androidx.compose.material:material-ripple:1.6.3 -androidx.compose.runtime:runtime-android:1.6.3 -androidx.compose.runtime:runtime-saveable-android:1.6.3 -androidx.compose.runtime:runtime-saveable:1.6.3 -androidx.compose.runtime:runtime:1.6.3 -androidx.compose.ui:ui-android:1.6.3 -androidx.compose.ui:ui-geometry-android:1.6.3 -androidx.compose.ui:ui-geometry:1.6.3 -androidx.compose.ui:ui-graphics-android:1.6.3 -androidx.compose.ui:ui-graphics:1.6.3 -androidx.compose.ui:ui-text-android:1.6.3 -androidx.compose.ui:ui-text:1.6.3 -androidx.compose.ui:ui-tooling-preview-android:1.6.3 -androidx.compose.ui:ui-tooling-preview:1.6.3 -androidx.compose.ui:ui-unit-android:1.6.3 -androidx.compose.ui:ui-unit:1.6.3 -androidx.compose.ui:ui-util-android:1.6.3 -androidx.compose.ui:ui-util:1.6.3 -androidx.compose.ui:ui:1.6.3 +androidx.compose.material:material-ripple-android:1.7.0-beta01 +androidx.compose.material:material-ripple:1.7.0-beta01 +androidx.compose.runtime:runtime-android:1.7.0-beta01 +androidx.compose.runtime:runtime-saveable-android:1.7.0-beta01 +androidx.compose.runtime:runtime-saveable:1.7.0-beta01 +androidx.compose.runtime:runtime:1.7.0-beta01 +androidx.compose.ui:ui-android:1.7.0-beta01 +androidx.compose.ui:ui-geometry-android:1.7.0-beta01 +androidx.compose.ui:ui-geometry:1.7.0-beta01 +androidx.compose.ui:ui-graphics-android:1.7.0-beta01 +androidx.compose.ui:ui-graphics:1.7.0-beta01 +androidx.compose.ui:ui-text-android:1.7.0-beta01 +androidx.compose.ui:ui-text:1.7.0-beta01 +androidx.compose.ui:ui-tooling-preview-android:1.7.0-beta01 +androidx.compose.ui:ui-tooling-preview:1.7.0-beta01 +androidx.compose.ui:ui-unit-android:1.7.0-beta01 +androidx.compose.ui:ui-unit:1.7.0-beta01 +androidx.compose.ui:ui-util-android:1.7.0-beta01 +androidx.compose.ui:ui-util:1.7.0-beta01 +androidx.compose.ui:ui:1.7.0-beta01 androidx.compose:compose-bom:2024.02.02 androidx.concurrent:concurrent-futures:1.1.0 -androidx.core:core-ktx:1.12.0 -androidx.core:core:1.12.0 +androidx.core:core-ktx:1.13.1 +androidx.core:core:1.13.1 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.0.0 androidx.emoji2:emoji2:1.3.0 androidx.exifinterface:exifinterface:1.3.7 androidx.fragment:fragment:1.5.1 +androidx.graphics:graphics-path:1.0.1 androidx.interpolator:interpolator:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.7.0 -androidx.lifecycle:lifecycle-common:2.7.0 -androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0 -androidx.lifecycle:lifecycle-livedata-core:2.7.0 -androidx.lifecycle:lifecycle-livedata:2.7.0 -androidx.lifecycle:lifecycle-process:2.7.0 -androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 -androidx.lifecycle:lifecycle-runtime:2.7.0 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 -androidx.lifecycle:lifecycle-viewmodel:2.7.0 +androidx.lifecycle:lifecycle-common-java8:2.8.0 +androidx.lifecycle:lifecycle-common-jvm:2.8.0 +androidx.lifecycle:lifecycle-common:2.8.0 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.0 +androidx.lifecycle:lifecycle-livedata-core:2.8.0 +androidx.lifecycle:lifecycle-livedata:2.8.0 +androidx.lifecycle:lifecycle-process:2.8.0 +androidx.lifecycle:lifecycle-runtime-android:2.8.0 +androidx.lifecycle:lifecycle-runtime-compose-android:2.8.0 +androidx.lifecycle:lifecycle-runtime-compose:2.8.0 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.0 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.0 +androidx.lifecycle:lifecycle-runtime:2.8.0 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.0 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.0 +androidx.lifecycle:lifecycle-viewmodel:2.8.0 androidx.loader:loader:1.0.0 androidx.metrics:metrics-performance:1.0.0-alpha04 androidx.profileinstaller:profileinstaller:1.3.1 @@ -79,12 +90,16 @@ androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 +androidx.window.extensions.core:core:1.0.0 +androidx.window:window-core-android:1.3.0-beta02 +androidx.window:window-core:1.3.0-beta02 +androidx.window:window:1.3.0-beta02 com.google.accompanist:accompanist-drawablepainter:0.32.0 com.google.code.findbugs:jsr305:3.0.2 -com.google.dagger:dagger-lint-aar:2.51 -com.google.dagger:dagger:2.51 -com.google.dagger:hilt-android:2.51 -com.google.dagger:hilt-core:2.51 +com.google.dagger:dagger-lint-aar:2.51.1 +com.google.dagger:dagger:2.51.1 +com.google.dagger:hilt-android:2.51.1 +com.google.dagger:hilt-core:2.51.1 com.google.guava:listenablefuture:1.0 com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okio:okio-jvm:3.8.0 @@ -94,10 +109,10 @@ io.coil-kt:coil-compose-base:2.6.0 io.coil-kt:coil-compose:2.6.0 io.coil-kt:coil:2.6.0 javax.inject:javax.inject:1 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.22 +org.jetbrains.kotlin:kotlin-stdlib-common:2.0.0 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 -org.jetbrains.kotlin:kotlin-stdlib:1.9.22 +org.jetbrains.kotlin:kotlin-stdlib:2.0.0 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 diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt index 6544acde6..d3e90da35 100644 --- a/app/dependencies/prodReleaseRuntimeClasspath.txt +++ b/app/dependencies/prodReleaseRuntimeClasspath.txt @@ -2,10 +2,10 @@ androidx.activity:activity-compose:1.8.2 androidx.activity:activity-ktx:1.8.2 androidx.activity:activity:1.8.2 androidx.annotation:annotation-experimental:1.4.0 -androidx.annotation:annotation-jvm:1.8.0-beta01 -androidx.annotation:annotation:1.8.0-beta01 -androidx.appcompat:appcompat-resources:1.6.1 -androidx.appcompat:appcompat:1.6.1 +androidx.annotation:annotation-jvm:1.8.0 +androidx.annotation:annotation:1.8.0 +androidx.appcompat:appcompat-resources:1.7.0 +androidx.appcompat:appcompat:1.7.0 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 androidx.autofill:autofill:1.0.0 @@ -13,54 +13,56 @@ androidx.browser:browser:1.8.0 androidx.collection:collection-jvm:1.4.0 androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 -androidx.compose.animation:animation-android:1.7.0-alpha06 -androidx.compose.animation:animation-core-android:1.7.0-alpha06 -androidx.compose.animation:animation-core:1.7.0-alpha06 -androidx.compose.animation:animation:1.7.0-alpha06 -androidx.compose.foundation:foundation-android:1.7.0-alpha06 -androidx.compose.foundation:foundation-layout-android:1.7.0-alpha06 -androidx.compose.foundation:foundation-layout:1.7.0-alpha06 -androidx.compose.foundation:foundation:1.7.0-alpha06 -androidx.compose.material3.adaptive:adaptive-android:1.0.0-alpha10 -androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0-alpha10 -androidx.compose.material3.adaptive:adaptive-layout:1.0.0-alpha10 -androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0-alpha10 -androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-alpha10 -androidx.compose.material3.adaptive:adaptive:1.0.0-alpha10 -androidx.compose.material3:material3-android:1.2.1 -androidx.compose.material3:material3-window-size-class-android:1.2.1 -androidx.compose.material3:material3-window-size-class:1.2.1 -androidx.compose.material3:material3:1.2.1 +androidx.compose.animation:animation-android:1.7.0-beta01 +androidx.compose.animation:animation-core-android:1.7.0-beta01 +androidx.compose.animation:animation-core:1.7.0-beta01 +androidx.compose.animation:animation:1.7.0-beta01 +androidx.compose.foundation:foundation-android:1.7.0-beta01 +androidx.compose.foundation:foundation-layout-android:1.7.0-beta01 +androidx.compose.foundation:foundation-layout:1.7.0-beta01 +androidx.compose.foundation:foundation:1.7.0-beta01 +androidx.compose.material3.adaptive:adaptive-android:1.0.0-beta01 +androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0-beta01 +androidx.compose.material3.adaptive:adaptive-layout:1.0.0-beta01 +androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0-beta01 +androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-beta01 +androidx.compose.material3.adaptive:adaptive:1.0.0-beta01 +androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.0-beta01 +androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0-beta01 +androidx.compose.material3:material3-android:1.3.0-beta01 +androidx.compose.material3:material3-window-size-class-android:1.3.0-beta01 +androidx.compose.material3:material3-window-size-class:1.3.0-beta01 +androidx.compose.material3:material3:1.3.0-beta01 androidx.compose.material:material-icons-core-android:1.6.3 androidx.compose.material:material-icons-core:1.6.3 androidx.compose.material:material-icons-extended-android:1.6.3 androidx.compose.material:material-icons-extended:1.6.3 -androidx.compose.material:material-ripple-android:1.6.3 -androidx.compose.material:material-ripple:1.6.3 -androidx.compose.runtime:runtime-android:1.7.0-alpha06 -androidx.compose.runtime:runtime-saveable-android:1.7.0-alpha06 -androidx.compose.runtime:runtime-saveable:1.7.0-alpha06 +androidx.compose.material:material-ripple-android:1.7.0-beta01 +androidx.compose.material:material-ripple:1.7.0-beta01 +androidx.compose.runtime:runtime-android:1.7.0-beta01 +androidx.compose.runtime:runtime-saveable-android:1.7.0-beta01 +androidx.compose.runtime:runtime-saveable:1.7.0-beta01 androidx.compose.runtime:runtime-tracing:1.0.0-beta01 -androidx.compose.runtime:runtime:1.7.0-alpha06 -androidx.compose.ui:ui-android:1.7.0-alpha06 -androidx.compose.ui:ui-geometry-android:1.7.0-alpha06 -androidx.compose.ui:ui-geometry:1.7.0-alpha06 -androidx.compose.ui:ui-graphics-android:1.7.0-alpha06 -androidx.compose.ui:ui-graphics:1.7.0-alpha06 -androidx.compose.ui:ui-text-android:1.7.0-alpha06 -androidx.compose.ui:ui-text:1.7.0-alpha06 -androidx.compose.ui:ui-tooling-preview-android:1.7.0-alpha06 -androidx.compose.ui:ui-tooling-preview:1.7.0-alpha06 -androidx.compose.ui:ui-unit-android:1.7.0-alpha06 -androidx.compose.ui:ui-unit:1.7.0-alpha06 -androidx.compose.ui:ui-util-android:1.7.0-alpha06 -androidx.compose.ui:ui-util:1.7.0-alpha06 -androidx.compose.ui:ui:1.7.0-alpha06 +androidx.compose.runtime:runtime:1.7.0-beta01 +androidx.compose.ui:ui-android:1.7.0-beta01 +androidx.compose.ui:ui-geometry-android:1.7.0-beta01 +androidx.compose.ui:ui-geometry:1.7.0-beta01 +androidx.compose.ui:ui-graphics-android:1.7.0-beta01 +androidx.compose.ui:ui-graphics:1.7.0-beta01 +androidx.compose.ui:ui-text-android:1.7.0-beta01 +androidx.compose.ui:ui-text:1.7.0-beta01 +androidx.compose.ui:ui-tooling-preview-android:1.7.0-beta01 +androidx.compose.ui:ui-tooling-preview:1.7.0-beta01 +androidx.compose.ui:ui-unit-android:1.7.0-beta01 +androidx.compose.ui:ui-unit:1.7.0-beta01 +androidx.compose.ui:ui-util-android:1.7.0-beta01 +androidx.compose.ui:ui-util:1.7.0-beta01 +androidx.compose.ui:ui:1.7.0-beta01 androidx.compose:compose-bom:2024.02.02 androidx.concurrent:concurrent-futures:1.1.0 -androidx.core:core-ktx:1.12.0 +androidx.core:core-ktx:1.13.1 androidx.core:core-splashscreen:1.0.1 -androidx.core:core:1.12.0 +androidx.core:core:1.13.1 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.0.0 @@ -73,33 +75,34 @@ androidx.drawerlayout:drawerlayout:1.0.0 androidx.emoji2:emoji2-views-helper:1.3.0 androidx.emoji2:emoji2:1.3.0 androidx.exifinterface:exifinterface:1.3.7 -androidx.fragment:fragment:1.5.1 -androidx.graphics:graphics-path:1.0.0-beta02 +androidx.fragment:fragment:1.5.4 +androidx.graphics:graphics-path:1.0.1 androidx.hilt:hilt-common:1.1.0 androidx.hilt:hilt-navigation-compose:1.2.0 androidx.hilt:hilt-navigation:1.2.0 androidx.hilt:hilt-work:1.1.0 androidx.interpolator:interpolator:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.8.0-alpha04 -androidx.lifecycle:lifecycle-common-jvm:2.8.0-alpha04 -androidx.lifecycle:lifecycle-common:2.8.0-alpha04 -androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.0-alpha04 -androidx.lifecycle:lifecycle-livedata-core:2.8.0-alpha04 -androidx.lifecycle:lifecycle-livedata:2.8.0-alpha04 -androidx.lifecycle:lifecycle-process:2.8.0-alpha04 -androidx.lifecycle:lifecycle-runtime-android:2.8.0-alpha04 -androidx.lifecycle:lifecycle-runtime-compose:2.8.0-alpha04 -androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.0-alpha04 -androidx.lifecycle:lifecycle-runtime-ktx:2.8.0-alpha04 -androidx.lifecycle:lifecycle-runtime:2.8.0-alpha04 -androidx.lifecycle:lifecycle-service:2.8.0-alpha04 -androidx.lifecycle:lifecycle-viewmodel-android:2.8.0-alpha04 -androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.0-alpha04 -androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0-alpha04 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0-alpha04 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.0-alpha04 -androidx.lifecycle:lifecycle-viewmodel:2.8.0-alpha04 +androidx.lifecycle:lifecycle-common-java8:2.8.1 +androidx.lifecycle:lifecycle-common-jvm:2.8.1 +androidx.lifecycle:lifecycle-common:2.8.1 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.1 +androidx.lifecycle:lifecycle-livedata-core:2.8.1 +androidx.lifecycle:lifecycle-livedata:2.8.1 +androidx.lifecycle:lifecycle-process:2.8.1 +androidx.lifecycle:lifecycle-runtime-android:2.8.1 +androidx.lifecycle:lifecycle-runtime-compose-android:2.8.1 +androidx.lifecycle:lifecycle-runtime-compose:2.8.1 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.1 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.1 +androidx.lifecycle:lifecycle-runtime:2.8.1 +androidx.lifecycle:lifecycle-service:2.8.1 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.1 +androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.1 +androidx.lifecycle:lifecycle-viewmodel-compose:2.8.1 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.1 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.1 +androidx.lifecycle:lifecycle-viewmodel:2.8.1 androidx.loader:loader:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.metrics:metrics-performance:1.0.0-alpha04 @@ -129,9 +132,9 @@ androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 androidx.window.extensions.core:core:1.0.0 -androidx.window:window-core-android:1.3.0-alpha03 -androidx.window:window-core:1.3.0-alpha03 -androidx.window:window:1.3.0-alpha03 +androidx.window:window-core-android:1.3.0-beta02 +androidx.window:window-core:1.3.0-beta02 +androidx.window:window:1.3.0-beta02 androidx.work:work-runtime-ktx:2.9.0 androidx.work:work-runtime:2.9.0 com.caverock:androidsvg-aar:1.4 @@ -154,10 +157,10 @@ com.google.android.gms:play-services-oss-licenses:17.0.1 com.google.android.gms:play-services-stats:17.0.2 com.google.android.gms:play-services-tasks:18.0.2 com.google.code.findbugs:jsr305:3.0.2 -com.google.dagger:dagger-lint-aar:2.51 -com.google.dagger:dagger:2.51 -com.google.dagger:hilt-android:2.51 -com.google.dagger:hilt-core:2.51 +com.google.dagger:dagger-lint-aar:2.51.1 +com.google.dagger:dagger:2.51.1 +com.google.dagger:hilt-android:2.51.1 +com.google.dagger:hilt-core:2.51.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 @@ -188,8 +191,8 @@ 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:4.26.0 -com.google.protobuf:protobuf-kotlin-lite:4.26.0 +com.google.protobuf:protobuf-javalite:4.26.1 +com.google.protobuf:protobuf-kotlin-lite:4.26.1 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 @@ -203,10 +206,10 @@ io.coil-kt:coil-svg:2.6.0 io.coil-kt:coil:2.6.0 javax.inject:javax.inject:1 org.checkerframework:checker-qual:3.12.0 -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.22 +org.jetbrains.kotlin:kotlin-stdlib-common:2.0.0 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 -org.jetbrains.kotlin:kotlin-stdlib:1.9.22 +org.jetbrains.kotlin:kotlin-stdlib:2.0.0 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.0 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9c7f3b935..5f4922bce 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,13 +1,3 @@ --dontwarn org.bouncycastle.jsse.BCSSLParameters --dontwarn org.bouncycastle.jsse.BCSSLSocket --dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider --dontwarn org.conscrypt.Conscrypt$Version --dontwarn org.conscrypt.Conscrypt --dontwarn org.conscrypt.ConscryptHostnameVerifier --dontwarn org.openjsse.javax.net.ssl.SSLParameters --dontwarn org.openjsse.javax.net.ssl.SSLSocket --dontwarn org.openjsse.net.ssl.OpenJSSE - # Fix for Retrofit issue https://github.com/square/retrofit/issues/3751 # Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). -keep,allowobfuscation,allowshrinking interface retrofit2.Call @@ -16,4 +6,4 @@ # With R8 full mode generic signatures are stripped for classes that are not # kept. Suspend functions are wrapped in continuations where the type argument # is used. --keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation \ No newline at end of file +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt index c9cc64120..93c674bcc 100644 --- a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -20,7 +20,6 @@ import androidx.annotation.StringRes import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.assertIsSelected -import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.AndroidComposeTestRule @@ -225,12 +224,7 @@ class NavigationTest { onNodeWithText(ok).performClick() // Check that the saved screen is still visible and selected. - onNode( - hasText(saved) and - hasAnyAncestor( - hasTestTag("NiaBottomBar") or hasTestTag("NiaNavRail"), - ), - ).assertIsSelected() + onNode(hasText(saved) and hasTestTag("NiaNavItem")).assertIsSelected() } } diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt deleted file mode 100644 index 03cf653ae..000000000 --- a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationUiTest.kt +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.samples.apps.nowinandroid.ui - -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.ui.test.DeviceConfigurationOverride -import androidx.compose.ui.test.ForcedSize -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository -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.rules.GrantPostNotificationsPermissionRule -import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository -import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository -import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity -import dagger.hilt.android.testing.BindValue -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import javax.inject.Inject - -/** - * Tests that the navigation UI is rendered correctly on different screen sizes. - */ -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) -@HiltAndroidTest -class NavigationUiTest { - - /** - * Manages the components' state and is used to perform injection on your test - */ - @get:Rule(order = 0) - val hiltRule = HiltAndroidRule(this) - - /** - * Create a temporary folder used to create a Data Store file. This guarantees that - * the file is removed in between each test, preventing a crash. - */ - @BindValue - @get:Rule(order = 1) - val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() - - /** - * Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission. - */ - @get:Rule(order = 2) - val postNotificationsPermission = GrantPostNotificationsPermissionRule() - - /** - * Use a test activity to set the content on. - */ - @get:Rule(order = 3) - val composeTestRule = createAndroidComposeRule() - - val userNewsResourceRepository = CompositeUserNewsResourceRepository( - newsRepository = TestNewsRepository(), - userDataRepository = TestUserDataRepository(), - ) - - @Inject - lateinit var networkMonitor: NetworkMonitor - - @Inject - lateinit var timeZoneMonitor: TimeZoneMonitor - - @Before - fun setup() { - hiltRule.inject() - } - - @Test - fun compactWidth_compactHeight_showsNavigationBar() { - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(400.dp, 400.dp)), - ) { - BoxWithConstraints { - NiaApp(fakeAppState(maxWidth, maxHeight)) - } - } - } - - composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed() - composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist() - } - - @Test - fun mediumWidth_compactHeight_showsNavigationRail() { - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(610.dp, 400.dp)), - ) { - BoxWithConstraints { - NiaApp(fakeAppState(maxWidth, maxHeight)) - } - } - } - - composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() - composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() - } - - @Test - fun expandedWidth_compactHeight_showsNavigationRail() { - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 400.dp)), - ) { - BoxWithConstraints { - NiaApp(fakeAppState(maxWidth, maxHeight)) - } - } - } - - composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() - composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() - } - - @Test - fun compactWidth_mediumHeight_showsNavigationBar() { - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(400.dp, 500.dp)), - ) { - BoxWithConstraints { - NiaApp(fakeAppState(maxWidth, maxHeight)) - } - } - } - - composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed() - composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist() - } - - @Test - fun mediumWidth_mediumHeight_showsNavigationRail() { - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(610.dp, 500.dp)), - ) { - BoxWithConstraints { - NiaApp(fakeAppState(maxWidth, maxHeight)) - } - } - } - - composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() - composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() - } - - @Test - fun expandedWidth_mediumHeight_showsNavigationRail() { - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 500.dp)), - ) { - BoxWithConstraints { - NiaApp(fakeAppState(maxWidth, maxHeight)) - } - } - } - - composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() - composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() - } - - @Test - fun compactWidth_expandedHeight_showsNavigationBar() { - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(400.dp, 1000.dp)), - ) { - BoxWithConstraints { - NiaApp(fakeAppState(maxWidth, maxHeight)) - } - } - } - - composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed() - composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist() - } - - @Test - fun mediumWidth_expandedHeight_showsNavigationRail() { - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(610.dp, 1000.dp)), - ) { - BoxWithConstraints { - NiaApp(fakeAppState(maxWidth, maxHeight)) - } - } - } - - composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() - composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() - } - - @Test - fun expandedWidth_expandedHeight_showsNavigationRail() { - composeTestRule.setContent { - DeviceConfigurationOverride( - DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1000.dp)), - ) { - BoxWithConstraints { - NiaApp(fakeAppState(maxWidth, maxHeight)) - } - } - } - - composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed() - composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist() - } - - @Composable - private fun fakeAppState(maxWidth: Dp, maxHeight: Dp) = rememberNiaAppState( - windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(maxWidth, maxHeight)), - networkMonitor = networkMonitor, - userNewsResourceRepository = userNewsResourceRepository, - timeZoneMonitor = timeZoneMonitor, - ) -} diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index 7c9dfcc7a..c2c74458d 100644 --- a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -16,15 +16,11 @@ package com.google.samples.apps.nowinandroid.ui -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.ComposeNavigator import androidx.navigation.compose.composable @@ -43,7 +39,6 @@ import kotlinx.datetime.TimeZone import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals -import kotlin.test.assertFalse import kotlin.test.assertTrue /** @@ -52,7 +47,6 @@ import kotlin.test.assertTrue * Note: This could become an unit test if Robolectric is added to the project and the Context * is faked. */ -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) class NiaAppStateTest { @get:Rule @@ -79,7 +73,6 @@ class NiaAppStateTest { NiaAppState( navController = navController, coroutineScope = backgroundScope, - windowSizeClass = getCompactWindowClass(), networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, @@ -102,7 +95,6 @@ class NiaAppStateTest { fun niaAppState_destinations() = runTest { composeTestRule.setContent { state = rememberNiaAppState( - windowSizeClass = getCompactWindowClass(), networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, @@ -115,64 +107,12 @@ class NiaAppStateTest { assertTrue(state.topLevelDestinations[2].name.contains("interests", true)) } - @Test - fun niaAppState_showBottomBar_compact() = runTest { - composeTestRule.setContent { - state = NiaAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = getCompactWindowClass(), - networkMonitor = networkMonitor, - userNewsResourceRepository = userNewsResourceRepository, - timeZoneMonitor = timeZoneMonitor, - ) - } - - assertTrue(state.shouldShowBottomBar) - assertFalse(state.shouldShowNavRail) - } - - @Test - fun niaAppState_showNavRail_medium() = runTest { - composeTestRule.setContent { - state = NiaAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)), - networkMonitor = networkMonitor, - userNewsResourceRepository = userNewsResourceRepository, - timeZoneMonitor = timeZoneMonitor, - ) - } - - assertTrue(state.shouldShowNavRail) - assertFalse(state.shouldShowBottomBar) - } - - @Test - fun niaAppState_showNavRail_large() = runTest { - composeTestRule.setContent { - state = NiaAppState( - navController = NavHostController(LocalContext.current), - coroutineScope = backgroundScope, - windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)), - networkMonitor = networkMonitor, - userNewsResourceRepository = userNewsResourceRepository, - timeZoneMonitor = timeZoneMonitor, - ) - } - - assertTrue(state.shouldShowNavRail) - assertFalse(state.shouldShowBottomBar) - } - @Test fun niaAppState_whenNetworkMonitorIsOffline_StateIsOffline() = runTest(UnconfinedTestDispatcher()) { composeTestRule.setContent { state = NiaAppState( navController = NavHostController(LocalContext.current), coroutineScope = backgroundScope, - windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)), networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, @@ -193,7 +133,6 @@ class NiaAppStateTest { state = NiaAppState( navController = NavHostController(LocalContext.current), coroutineScope = backgroundScope, - windowSizeClass = getCompactWindowClass(), networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, @@ -207,8 +146,6 @@ class NiaAppStateTest { state.currentTimeZone.value, ) } - - private fun getCompactWindowClass() = WindowSizeClass.calculateFromSize(DpSize(500.dp, 300.dp)) } @Composable diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt index ad95c297f..2f8572102 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt @@ -23,8 +23,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect @@ -58,7 +57,6 @@ import javax.inject.Inject private const val TAG = "MainActivity" -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -134,7 +132,6 @@ class MainActivity : ComponentActivity() { } val appState = rememberNiaAppState( - windowSizeClass = calculateWindowSizeClass(this), networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, @@ -151,6 +148,7 @@ class MainActivity : ComponentActivity() { androidTheme = shouldUseAndroidTheme(uiState), disableDynamicTheming = shouldDisableDynamicTheming(uiState), ) { + @OptIn(ExperimentalMaterial3AdaptiveApi::class) NiaApp(appState) } } diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 4c8232a26..b47984ddb 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -18,7 +18,6 @@ package com.google.samples.apps.nowinandroid.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets @@ -26,7 +25,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -39,6 +37,9 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.WindowAdaptiveInfo +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -63,10 +64,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground -import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar -import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBarItem -import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationRail -import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationRailItem +import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationSuiteScaffold import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors @@ -76,8 +74,13 @@ import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable -fun NiaApp(appState: NiaAppState, modifier: Modifier = Modifier) { +fun NiaApp( + appState: NiaAppState, + modifier: Modifier = Modifier, + windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), +) { val shouldShowGradientBackground = appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU var showSettingsDialog by rememberSaveable { mutableStateOf(false) } @@ -111,13 +114,18 @@ fun NiaApp(appState: NiaAppState, modifier: Modifier = Modifier) { showSettingsDialog = showSettingsDialog, onSettingsDismissed = { showSettingsDialog = false }, onTopAppBarActionClick = { showSettingsDialog = true }, + windowAdaptiveInfo = windowAdaptiveInfo, ) } } } @Composable -@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalComposeUiApi::class, + ExperimentalMaterial3AdaptiveApi::class, +) internal fun NiaApp( appState: NiaAppState, snackbarHostState: SnackbarHostState, @@ -125,59 +133,69 @@ internal fun NiaApp( onSettingsDismissed: () -> Unit, onTopAppBarActionClick: () -> Unit, modifier: Modifier = Modifier, + windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { val unreadDestinations by appState.topLevelDestinationsWithUnreadResources .collectAsStateWithLifecycle() + val currentDestination = appState.currentDestination if (showSettingsDialog) { SettingsDialog( onDismiss = { onSettingsDismissed() }, ) } - Scaffold( - modifier = modifier.semantics { - testTagsAsResourceId = true - }, - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onBackground, - contentWindowInsets = WindowInsets(0, 0, 0, 0), - snackbarHost = { SnackbarHost(snackbarHostState) }, - bottomBar = { - if (appState.shouldShowBottomBar) { - NiaBottomBar( - destinations = appState.topLevelDestinations, - destinationsWithUnreadResources = unreadDestinations, - onNavigateToDestination = appState::navigateToTopLevelDestination, - currentDestination = appState.currentDestination, - modifier = Modifier.testTag("NiaBottomBar"), + + NiaNavigationSuiteScaffold( + navigationSuiteItems = { + appState.topLevelDestinations.forEach { destination -> + val hasUnread = unreadDestinations.contains(destination) + val selected = currentDestination + .isTopLevelDestinationInHierarchy(destination) + item( + selected = selected, + onClick = { appState.navigateToTopLevelDestination(destination) }, + icon = { + Icon( + imageVector = destination.unselectedIcon, + contentDescription = null, + ) + }, + selectedIcon = { + Icon( + imageVector = destination.selectedIcon, + contentDescription = null, + ) + }, + label = { Text(stringResource(destination.iconTextId)) }, + modifier = + Modifier + .testTag("NiaNavItem") + .then(if (hasUnread) Modifier.notificationDot() else Modifier), ) } }, - ) { padding -> - Row( - Modifier - .fillMaxSize() - .padding(padding) - .consumeWindowInsets(padding) - .windowInsetsPadding( - WindowInsets.safeDrawing.only( - WindowInsetsSides.Horizontal, + windowAdaptiveInfo = windowAdaptiveInfo, + ) { + Scaffold( + modifier = modifier.semantics { + testTagsAsResourceId = true + }, + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground, + contentWindowInsets = WindowInsets(0, 0, 0, 0), + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { padding -> + Column( + Modifier + .fillMaxSize() + .padding(padding) + .consumeWindowInsets(padding) + .windowInsetsPadding( + WindowInsets.safeDrawing.only( + WindowInsetsSides.Horizontal, + ), ), - ), - ) { - if (appState.shouldShowNavRail) { - NiaNavRail( - destinations = appState.topLevelDestinations, - destinationsWithUnreadResources = unreadDestinations, - onNavigateToDestination = appState::navigateToTopLevelDestination, - currentDestination = appState.currentDestination, - modifier = Modifier - .testTag("NiaNavRail") - .safeDrawingPadding(), - ) - } - - Column(Modifier.fillMaxSize()) { + ) { // Show the top app bar on top level destinations. val destination = appState.currentTopLevelDestination val shouldShowTopAppBar = destination != null @@ -201,13 +219,14 @@ internal fun NiaApp( } Box( - modifier = if (shouldShowTopAppBar) { - Modifier.consumeWindowInsets( - WindowInsets.safeDrawing.only(WindowInsetsSides.Top), - ) - } else { - Modifier - }, + // Workaround for https://issuetracker.google.com/338478720 + modifier = Modifier.consumeWindowInsets( + if (shouldShowTopAppBar) { + WindowInsets.safeDrawing.only(WindowInsetsSides.Top) + } else { + WindowInsets(0, 0, 0, 0) + }, + ), ) { NiaNavHost( appState = appState, @@ -220,80 +239,10 @@ internal fun NiaApp( }, ) } - } - // TODO: We may want to add padding or spacer when the snackbar is shown so that - // content doesn't display behind it. - } - } -} - -@Composable -private fun NiaNavRail( - destinations: List, - destinationsWithUnreadResources: Set, - onNavigateToDestination: (TopLevelDestination) -> Unit, - currentDestination: NavDestination?, - modifier: Modifier = Modifier, -) { - NiaNavigationRail(modifier = modifier) { - destinations.forEach { destination -> - val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) - val hasUnread = destinationsWithUnreadResources.contains(destination) - NiaNavigationRailItem( - selected = selected, - onClick = { onNavigateToDestination(destination) }, - icon = { - Icon( - imageVector = destination.unselectedIcon, - contentDescription = null, - ) - }, - selectedIcon = { - Icon( - imageVector = destination.selectedIcon, - contentDescription = null, - ) - }, - label = { Text(stringResource(destination.iconTextId)) }, - modifier = if (hasUnread) Modifier.notificationDot() else Modifier, - ) - } - } -} - -@Composable -private fun NiaBottomBar( - destinations: List, - destinationsWithUnreadResources: Set, - onNavigateToDestination: (TopLevelDestination) -> Unit, - currentDestination: NavDestination?, - modifier: Modifier = Modifier, -) { - NiaNavigationBar( - modifier = modifier, - ) { - destinations.forEach { destination -> - val hasUnread = destinationsWithUnreadResources.contains(destination) - val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) - NiaNavigationBarItem( - selected = selected, - onClick = { onNavigateToDestination(destination) }, - icon = { - Icon( - imageVector = destination.unselectedIcon, - contentDescription = null, - ) - }, - selectedIcon = { - Icon( - imageVector = destination.selectedIcon, - contentDescription = null, - ) - }, - label = { Text(stringResource(destination.iconTextId)) }, - modifier = if (hasUnread) Modifier.notificationDot() else Modifier, - ) + // TODO: We may want to add padding or spacer when the snackbar is shown so that + // content doesn't display behind it. + } } } } diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt index b653d8910..519603579 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt @@ -16,8 +16,6 @@ package com.google.samples.apps.nowinandroid.ui -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember @@ -55,7 +53,6 @@ import kotlinx.datetime.TimeZone @Composable fun rememberNiaAppState( - windowSizeClass: WindowSizeClass, networkMonitor: NetworkMonitor, userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, @@ -66,7 +63,6 @@ fun rememberNiaAppState( return remember( navController, coroutineScope, - windowSizeClass, networkMonitor, userNewsResourceRepository, timeZoneMonitor, @@ -74,7 +70,6 @@ fun rememberNiaAppState( NiaAppState( navController = navController, coroutineScope = coroutineScope, - windowSizeClass = windowSizeClass, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, @@ -86,7 +81,6 @@ fun rememberNiaAppState( class NiaAppState( val navController: NavHostController, coroutineScope: CoroutineScope, - val windowSizeClass: WindowSizeClass, networkMonitor: NetworkMonitor, userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, @@ -103,12 +97,6 @@ class NiaAppState( else -> null } - val shouldShowBottomBar: Boolean - get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact - - val shouldShowNavRail: Boolean - get() = !shouldShowBottomBar - val isOffline = networkMonitor.isOnline .map(Boolean::not) .stateIn( diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/DeviceConfigurationOverrideWindowInsets.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/DeviceConfigurationOverrideWindowInsets.kt new file mode 100644 index 000000000..2fc88e561 --- /dev/null +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/DeviceConfigurationOverrideWindowInsets.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.ui + +import android.view.WindowInsets +import android.widget.FrameLayout +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.test.DeviceConfigurationOverride +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.children + +/** + * A [DeviceConfigurationOverride] that allows overriding the [windowInsets] available + * to the content under test. + */ +@Suppress("ktlint:standard:function-naming") +fun DeviceConfigurationOverride.Companion.WindowInsets( + windowInsets: WindowInsetsCompat, +): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest -> + val currentContentUnderTest by rememberUpdatedState(contentUnderTest) + val currentWindowInsets by rememberUpdatedState(windowInsets) + AndroidView( + factory = { context -> + object : FrameLayout(context) { + override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { + children.forEach { + it.dispatchApplyWindowInsets(currentWindowInsets.toWindowInsets()) + } + return WindowInsetsCompat.CONSUMED.toWindowInsets()!! + } + + /** + * Deprecated, but intercept the `requestApplyInsets` call via the deprecated + * method. + */ + @Deprecated("Deprecated in Java") + override fun requestFitSystemWindows() { + dispatchApplyWindowInsets(currentWindowInsets.toWindowInsets()!!) + } + }.apply { + addView( + ComposeView(context).apply { + setContent { + currentContentUnderTest() + } + }, + ) + } + }, + ) +} diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt index 32bdfe9d9..1cca5a13a 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt @@ -16,8 +16,9 @@ package com.google.samples.apps.nowinandroid.ui -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.Posture +import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.test.DeviceConfigurationOverride @@ -27,6 +28,7 @@ import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowSizeClass import com.github.takahirom.roborazzi.captureRoboImage import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository @@ -57,7 +59,6 @@ import javax.inject.Inject /** * Tests that the navigation UI is rendered correctly on different screen sizes. */ -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) // Configure Robolectric to use a very large screen size that can fit all of the test sizes. @@ -122,6 +123,7 @@ class NiaAppScreenSizesScreenshotTests { TimeZone.setDefault(TimeZone.getTimeZone("UTC")) } + @OptIn(ExperimentalMaterial3AdaptiveApi::class) private fun testNiaAppScreenshotWithSize(width: Dp, height: Dp, screenshotName: String) { composeTestRule.setContent { CompositionLocalProvider( @@ -132,14 +134,20 @@ class NiaAppScreenSizesScreenshotTests { ) { NiaTheme { val fakeAppState = rememberNiaAppState( - windowSizeClass = WindowSizeClass.calculateFromSize( - DpSize(width, height), - ), networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) - NiaApp(fakeAppState) + NiaApp( + fakeAppState, + windowAdaptiveInfo = WindowAdaptiveInfo( + windowSizeClass = WindowSizeClass.compute( + width.value, + height.value, + ), + windowPosture = Posture(), + ), + ) } } } @@ -162,20 +170,20 @@ class NiaAppScreenSizesScreenshotTests { } @Test - fun mediumWidth_compactHeight_showsNavigationRail() { + fun mediumWidth_compactHeight_showsNavigationBar() { testNiaAppScreenshotWithSize( 610.dp, 400.dp, - "mediumWidth_compactHeight_showsNavigationRail", + "mediumWidth_compactHeight_showsNavigationBar", ) } @Test - fun expandedWidth_compactHeight_showsNavigationRail() { + fun expandedWidth_compactHeight_showsNavigationBar() { testNiaAppScreenshotWithSize( 900.dp, 400.dp, - "expandedWidth_compactHeight_showsNavigationRail", + "expandedWidth_compactHeight_showsNavigationBar", ) } diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt new file mode 100644 index 000000000..b9970effd --- /dev/null +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt @@ -0,0 +1,349 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsEndWidth +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.windowInsetsStartWidth +import androidx.compose.foundation.layout.windowInsetsTopHeight +import androidx.compose.material3.SnackbarDuration.Indefinite +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.Posture +import androidx.compose.material3.adaptive.WindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toAndroidRect +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.DeviceConfigurationOverride +import androidx.compose.ui.test.ForcedSize +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpRect +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.roundToIntRect +import androidx.core.graphics.Insets +import androidx.core.view.WindowInsetsCompat +import androidx.window.core.layout.WindowSizeClass +import com.github.takahirom.roborazzi.captureRoboImage +import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository +import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository +import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository +import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor +import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor +import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme +import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions +import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode +import org.robolectric.annotation.LooperMode +import java.util.TimeZone +import javax.inject.Inject + +/** + * Tests that the Snackbar is correctly displayed on different screen sizes. + */ +@RunWith(RobolectricTestRunner::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +// 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. +@Config(application = HiltTestApplication::class, qualifiers = "w1000dp-h1000dp-480dpi") +@LooperMode(LooperMode.Mode.PAUSED) +@HiltAndroidTest +class SnackbarInsetsScreenshotTests { + + /** + * Manages the components' state and is used to perform injection on your test + */ + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + /** + * Create a temporary folder used to create a Data Store file. This guarantees that + * the file is removed in between each test, preventing a crash. + */ + @BindValue + @get:Rule(order = 1) + val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() + + /** + * Use a test activity to set the content on. + */ + @get:Rule(order = 2) + val composeTestRule = createAndroidComposeRule() + + @Inject + lateinit var networkMonitor: NetworkMonitor + + @Inject + lateinit var timeZoneMonitor: TimeZoneMonitor + + @Inject + lateinit var userDataRepository: FakeUserDataRepository + + @Inject + lateinit var topicsRepository: TopicsRepository + + @Inject + lateinit var userNewsResourceRepository: UserNewsResourceRepository + + @Before + fun setup() { + hiltRule.inject() + + // Configure user data + runBlocking { + userDataRepository.setShouldHideOnboarding(true) + + userDataRepository.setFollowedTopicIds( + setOf(topicsRepository.getTopics().first().first().id), + ) + } + } + + @Before + fun setTimeZone() { + // Make time zone deterministic in tests + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + } + + @Test + fun phone_noSnackbar() { + val snackbarHostState = SnackbarHostState() + testSnackbarScreenshotWithSize( + snackbarHostState, + 400.dp, + 500.dp, + "insets_snackbar_compact_medium_noSnackbar", + action = { }, + ) + } + + @Test + fun snackbarShown_phone() { + val snackbarHostState = SnackbarHostState() + testSnackbarScreenshotWithSize( + snackbarHostState, + 400.dp, + 500.dp, + "insets_snackbar_compact_medium", + ) { + snackbarHostState.showSnackbar( + "This is a test snackbar message", + actionLabel = "Action Label", + duration = Indefinite, + ) + } + } + + @Test + fun snackbarShown_foldable() { + val snackbarHostState = SnackbarHostState() + testSnackbarScreenshotWithSize( + snackbarHostState, + 600.dp, + 600.dp, + "insets_snackbar_medium_medium", + ) { + snackbarHostState.showSnackbar( + "This is a test snackbar message", + actionLabel = "Action Label", + duration = Indefinite, + ) + } + } + + @Test + fun snackbarShown_tablet() { + val snackbarHostState = SnackbarHostState() + testSnackbarScreenshotWithSize( + snackbarHostState, + 900.dp, + 900.dp, + "insets_snackbar_expanded_expanded", + ) { + snackbarHostState.showSnackbar( + "This is a test snackbar message", + actionLabel = "Action Label", + duration = Indefinite, + ) + } + } + + @OptIn(ExperimentalMaterial3AdaptiveApi::class) + private fun testSnackbarScreenshotWithSize( + snackbarHostState: SnackbarHostState, + width: Dp, + height: Dp, + screenshotName: String, + action: suspend () -> Unit, + ) { + lateinit var scope: CoroutineScope + composeTestRule.setContent { + CompositionLocalProvider( + // Replaces images with placeholders + LocalInspectionMode provides true, + ) { + scope = rememberCoroutineScope() + + DeviceConfigurationOverride( + DeviceConfigurationOverride.ForcedSize(DpSize(width, height)), + ) { + DeviceConfigurationOverride( + DeviceConfigurationOverride.WindowInsets( + WindowInsetsCompat.Builder() + .setInsets( + WindowInsetsCompat.Type.statusBars(), + DpRect( + left = 0.dp, + top = 64.dp, + right = 0.dp, + bottom = 0.dp, + ).toInsets(), + ) + .setInsets( + WindowInsetsCompat.Type.navigationBars(), + DpRect( + left = 64.dp, + top = 0.dp, + right = 64.dp, + bottom = 64.dp, + ).toInsets(), + ) + .build(), + ), + ) { + BoxWithConstraints(Modifier.testTag("root")) { + NiaTheme { + val appState = rememberNiaAppState( + networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, + timeZoneMonitor = timeZoneMonitor, + ) + NiaApp( + appState = appState, + snackbarHostState = snackbarHostState, + showSettingsDialog = false, + onSettingsDismissed = {}, + onTopAppBarActionClick = {}, + windowAdaptiveInfo = WindowAdaptiveInfo( + windowSizeClass = WindowSizeClass.compute( + maxWidth.value, + maxHeight.value, + ), + windowPosture = Posture(), + ), + ) + DebugVisibleWindowInsets() + } + } + } + } + } + } + + scope.launch { + action() + } + + composeTestRule.onNodeWithTag("root") + .captureRoboImage( + "src/testDemo/screenshots/$screenshotName.png", + roborazziOptions = DefaultRoborazziOptions, + ) + } +} + +@Composable +fun DebugVisibleWindowInsets( + modifier: Modifier = Modifier, + debugColor: Color = Color.Magenta.copy(alpha = 0.5f), +) { + Box(modifier = modifier.fillMaxSize()) { + Spacer( + modifier = Modifier + .align(Alignment.CenterStart) + .fillMaxHeight() + .windowInsetsStartWidth(WindowInsets.safeDrawing) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical)) + .background(debugColor), + ) + Spacer( + modifier = Modifier + .align(Alignment.CenterEnd) + .fillMaxHeight() + .windowInsetsEndWidth(WindowInsets.safeDrawing) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical)) + .background(debugColor), + ) + Spacer( + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth() + .windowInsetsTopHeight(WindowInsets.safeDrawing) + .background(debugColor), + ) + Spacer( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .windowInsetsBottomHeight(WindowInsets.safeDrawing) + .background(debugColor), + ) + } +} + +@Composable +private fun DpRect.toInsets() = toInsets(LocalDensity.current) + +private fun DpRect.toInsets(density: Density) = + Insets.of(with(density) { toRect() }.roundToIntRect().toAndroidRect()) diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt index ab67f8399..6f12dd620 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt @@ -19,8 +19,9 @@ package com.google.samples.apps.nowinandroid.ui import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.Posture +import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalInspectionMode @@ -31,6 +32,7 @@ import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowSizeClass import com.github.takahirom.roborazzi.captureRoboImage import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository @@ -63,7 +65,6 @@ import javax.inject.Inject /** * Tests that the Snackbar is correctly displayed on different screen sizes. */ -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) // Configure Robolectric to use a very large screen size that can fit all of the test sizes. @@ -191,6 +192,7 @@ class SnackbarScreenshotTests { } } + @OptIn(ExperimentalMaterial3AdaptiveApi::class) private fun testSnackbarScreenshotWithSize( snackbarHostState: SnackbarHostState, width: Dp, @@ -210,16 +212,26 @@ class SnackbarScreenshotTests { DeviceConfigurationOverride.ForcedSize(DpSize(width, height)), ) { BoxWithConstraints { - val appState = rememberNiaAppState( - windowSizeClass = WindowSizeClass.calculateFromSize( - DpSize(maxWidth, maxHeight), - ), - networkMonitor = networkMonitor, - userNewsResourceRepository = userNewsResourceRepository, - timeZoneMonitor = timeZoneMonitor, - ) NiaTheme { - NiaApp(appState, snackbarHostState, false, {}, {}) + val appState = rememberNiaAppState( + networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, + timeZoneMonitor = timeZoneMonitor, + ) + NiaApp( + appState = appState, + snackbarHostState = snackbarHostState, + showSettingsDialog = false, + onSettingsDismissed = {}, + onTopAppBarActionClick = {}, + windowAdaptiveInfo = WindowAdaptiveInfo( + windowSizeClass = WindowSizeClass.compute( + maxWidth.value, + maxHeight.value, + ), + windowPosture = Posture(), + ), + ) } } } diff --git a/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png index 011a97e37..912fca4c7 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png index 279efe6d0..e052b5920 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png index 0754d5b35..668d69146 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png new file mode 100644 index 000000000..1daf5ec34 Binary files /dev/null and b/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationRail.png deleted file mode 100644 index f4dfb09aa..000000000 Binary files a/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationRail.png and /dev/null differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_compact_medium.png b/app/src/testDemo/screenshots/insets_snackbar_compact_medium.png new file mode 100644 index 000000000..aae785a47 Binary files /dev/null and b/app/src/testDemo/screenshots/insets_snackbar_compact_medium.png differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png b/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png new file mode 100644 index 000000000..d37f02c65 Binary files /dev/null and b/app/src/testDemo/screenshots/insets_snackbar_compact_medium_noSnackbar.png differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png new file mode 100644 index 000000000..3d2c79256 Binary files /dev/null and b/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png b/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png new file mode 100644 index 000000000..3e7171bf4 Binary files /dev/null and b/app/src/testDemo/screenshots/insets_snackbar_medium_medium.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png new file mode 100644 index 000000000..4bc5d2b1c Binary files /dev/null and b/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationRail.png deleted file mode 100644 index 5ed3d9445..000000000 Binary files a/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationRail.png and /dev/null differ diff --git a/app/src/testDemo/screenshots/snackbar_compact_medium.png b/app/src/testDemo/screenshots/snackbar_compact_medium.png index 147c9ce6b..7676de40a 100644 Binary files a/app/src/testDemo/screenshots/snackbar_compact_medium.png and b/app/src/testDemo/screenshots/snackbar_compact_medium.png differ diff --git a/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png b/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png index 2767ff9b5..ff9ed7669 100644 Binary files a/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png and b/app/src/testDemo/screenshots/snackbar_compact_medium_noSnackbar.png differ diff --git a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png index 5360d68de..4997a83af 100644 Binary files a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png and b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png differ diff --git a/app/src/testDemo/screenshots/snackbar_medium_medium.png b/app/src/testDemo/screenshots/snackbar_medium_medium.png index b60112101..36fffa9c6 100644 Binary files a/app/src/testDemo/screenshots/snackbar_medium_medium.png and b/app/src/testDemo/screenshots/snackbar_medium_medium.png differ diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index aa0e615ad..dc478a829 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -14,6 +14,7 @@ * limitations under the License. */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -28,15 +29,17 @@ java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } -tasks.withType().configureEach { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 } } dependencies { compileOnly(libs.android.gradlePlugin) compileOnly(libs.android.tools.common) + compileOnly(libs.compose.gradlePlugin) compileOnly(libs.firebase.crashlytics.gradlePlugin) compileOnly(libs.firebase.performance.gradlePlugin) compileOnly(libs.kotlin.gradlePlugin) diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt index 3eeed97cf..a8b1b1779 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -18,12 +18,14 @@ import com.android.build.api.dsl.ApplicationExtension import com.google.samples.apps.nowinandroid.configureAndroidCompose import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.getByType class AndroidApplicationComposeConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("com.android.application") + apply(plugin = "com.android.application") + apply(plugin = "org.jetbrains.kotlin.plugin.compose") val extension = extensions.getByType() configureAndroidCompose(extension) diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index dd9eead63..19fabf549 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -18,14 +18,14 @@ import com.android.build.gradle.LibraryExtension import com.google.samples.apps.nowinandroid.configureAndroidCompose import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.getByType -import org.gradle.kotlin.dsl.kotlin class AndroidLibraryComposeConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("com.android.library") + apply(plugin = "com.android.library") + apply(plugin = "org.jetbrains.kotlin.plugin.compose") val extension = extensions.getByType() configureAndroidCompose(extension) diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt index e38c5b300..f16a8051a 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt @@ -18,9 +18,11 @@ package com.google.samples.apps.nowinandroid import com.android.build.api.dsl.CommonExtension import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.assign +import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension /** * Configure Compose-specific options @@ -33,10 +35,6 @@ internal fun Project.configureAndroidCompose( compose = true } - composeOptions { - kotlinCompilerExtensionVersion = libs.findVersion("androidxComposeCompiler").get().toString() - } - dependencies { val bom = libs.findLibrary("androidx-compose-bom").get() add("implementation", platform(bom)) @@ -53,48 +51,22 @@ internal fun Project.configureAndroidCompose( } } - tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs += buildComposeMetricsParameters() - freeCompilerArgs += stabilityConfiguration() - freeCompilerArgs += strongSkippingConfiguration() - } - } -} + extensions.configure { + fun Provider.onlyIfTrue() = flatMap { provider { it.takeIf(String::toBoolean) } } + fun Provider<*>.relativeToRootProject(dir: String) = flatMap { + rootProject.layout.buildDirectory.dir(projectDir.toRelativeString(rootDir)) + }.map { it.dir(dir) } -private fun Project.buildComposeMetricsParameters(): List { - val metricParameters = mutableListOf() - val enableMetricsProvider = project.providers.gradleProperty("enableComposeCompilerMetrics") - val relativePath = projectDir.relativeTo(rootDir) - val buildDir = layout.buildDirectory.get().asFile - val enableMetrics = (enableMetricsProvider.orNull == "true") - if (enableMetrics) { - val metricsFolder = buildDir.resolve("compose-metrics").resolve(relativePath) - metricParameters.add("-P") - metricParameters.add( - "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath, - ) - } + project.providers.gradleProperty("enableComposeCompilerMetrics").onlyIfTrue() + .relativeToRootProject("compose-metrics") + .let(metricsDestination::set) - val enableReportsProvider = project.providers.gradleProperty("enableComposeCompilerReports") - val enableReports = (enableReportsProvider.orNull == "true") - if (enableReports) { - val reportsFolder = buildDir.resolve("compose-reports").resolve(relativePath) - metricParameters.add("-P") - metricParameters.add( - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath - ) - } + project.providers.gradleProperty("enableComposeCompilerReports").onlyIfTrue() + .relativeToRootProject("compose-reports") + .let(reportsDestination::set) - return metricParameters.toList() + stabilityConfigurationFile = rootProject.layout.projectDirectory.file("compose_compiler_config.conf") + + enableStrongSkippingMode = true + } } - -private fun Project.stabilityConfiguration() = listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=${project.rootDir.absolutePath}/compose_compiler_config.conf", -) - -private fun Project.strongSkippingConfiguration() = listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true", -) diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt index c59d3ffb8..4447b8602 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt @@ -16,10 +16,10 @@ package com.google.samples.apps.nowinandroid +import com.android.SdkConstants import com.android.build.api.artifact.SingleArtifact import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.gradle.BaseExtension -import com.android.SdkConstants import com.google.common.truth.Truth.assertWithMessage import org.gradle.api.DefaultTask import org.gradle.api.Project @@ -36,6 +36,7 @@ import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.gradle.configurationcache.extensions.capitalized +import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.register import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.process.ExecOperations @@ -117,23 +118,20 @@ fun Project.configureBadgingTasks( val generateBadgingTaskName = "generate${capitalizedVariantName}Badging" val generateBadging = tasks.register(generateBadgingTaskName) { - apk.set( - variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE), - ) - aapt2Executable.set( - File( - baseExtension.sdkDirectory, - "${SdkConstants.FD_BUILD_TOOLS}/" + - "${baseExtension.buildToolsVersion}/" + - SdkConstants.FN_AAPT2, - ), + apk = variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE) + + aapt2Executable = File( + baseExtension.sdkDirectory, + "${SdkConstants.FD_BUILD_TOOLS}/" + + "${baseExtension.buildToolsVersion}/" + + SdkConstants.FN_AAPT2, ) - badging.set( - project.layout.buildDirectory.file( - "outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt", - ), + + badging = project.layout.buildDirectory.file( + "outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt", ) + } val updateBadgingTaskName = "update${capitalizedVariantName}Badging" @@ -144,17 +142,14 @@ fun Project.configureBadgingTasks( val checkBadgingTaskName = "check${capitalizedVariantName}Badging" tasks.register(checkBadgingTaskName) { - goldenBadging.set( - project.layout.projectDirectory.file("${variant.name}-badging.txt"), - ) - generatedBadging.set( - generateBadging.get().badging, - ) - this.updateBadgingTaskName.set(updateBadgingTaskName) + goldenBadging = project.layout.projectDirectory.file("${variant.name}-badging.txt") + + generatedBadging = generateBadging.get().badging + + this.updateBadgingTaskName = updateBadgingTaskName + + output = project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName") - output.set( - project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName"), - ) } } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt index 7820a978e..972d539c6 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt @@ -24,6 +24,7 @@ import org.gradle.api.file.Directory import org.gradle.api.file.RegularFile import org.gradle.api.provider.ListProperty import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.withType @@ -66,9 +67,13 @@ internal fun Project.configureJacoco( val myObjFactory = project.objects val buildDir = layout.buildDirectory.get().asFile val allJars: ListProperty = myObjFactory.listProperty(RegularFile::class.java) - val allDirectories: ListProperty = myObjFactory.listProperty(Directory::class.java) + val allDirectories: ListProperty = + myObjFactory.listProperty(Directory::class.java) val reportTask = - tasks.register("create${variant.name.capitalize()}CombinedCoverageReport", JacocoReport::class) { + tasks.register( + "create${variant.name.capitalize()}CombinedCoverageReport", + JacocoReport::class, + ) { classDirectories.setFrom( allJars, @@ -76,23 +81,28 @@ internal fun Project.configureJacoco( dirs.map { dir -> myObjFactory.fileTree().setDir(dir).exclude(coverageExclusions) } - } + }, ) reports { - xml.required.set(true) - html.required.set(true) + xml.required = true + html.required = true } // TODO: This is missing files in src/debug/, src/prod, src/demo, src/demoDebug... - sourceDirectories.setFrom(files("$projectDir/src/main/java", "$projectDir/src/main/kotlin")) + sourceDirectories.setFrom( + files( + "$projectDir/src/main/java", + "$projectDir/src/main/kotlin", + ), + ) executionData.setFrom( project.fileTree("$buildDir/outputs/unit_test_code_coverage/${variant.name}UnitTest") .matching { include("**/*.exec") }, project.fileTree("$buildDir/outputs/code_coverage/${variant.name}AndroidTest") - .matching { include("**/*.ec") } - ) + .matching { include("**/*.ec") }, + ) } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt index f9a6717c3..bfb799595 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt @@ -20,11 +20,14 @@ import com.android.build.api.dsl.CommonExtension import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.provideDelegate -import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension /** * Configure base Kotlin with Android options @@ -48,7 +51,7 @@ internal fun Project.configureKotlinAndroid( } } - configureKotlin() + configureKotlin() dependencies { add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get()) @@ -66,26 +69,26 @@ internal fun Project.configureKotlinJvm() { targetCompatibility = JavaVersion.VERSION_11 } - configureKotlin() + configureKotlin() } /** * Configure base Kotlin options */ -private fun Project.configureKotlin() { - // Use withType to workaround https://youtrack.jetbrains.com/issue/KT-55947 - tasks.withType().configureEach { - kotlinOptions { - // Set JVM target to 11 - jvmTarget = JavaVersion.VERSION_11.toString() - // Treat all Kotlin warnings as errors (disabled by default) - // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties - val warningsAsErrors: String? by project - allWarningsAsErrors = warningsAsErrors.toBoolean() - freeCompilerArgs = freeCompilerArgs + listOf( - // Enable experimental coroutines APIs, including Flow - "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", - ) - } +private inline fun Project.configureKotlin() = configure { + // Treat all Kotlin warnings as errors (disabled by default) + // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties + val warningsAsErrors: String? by project + when (this) { + is KotlinAndroidProjectExtension -> compilerOptions + is KotlinJvmProjectExtension -> compilerOptions + else -> TODO("Unsupported project extension $this ${T::class}") + }.apply { + jvmTarget = JvmTarget.JVM_11 + allWarningsAsErrors = warningsAsErrors.toBoolean() + freeCompilerArgs.add( + // Enable experimental coroutines APIs, including Flow + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + ) } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/PrintTestApks.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/PrintTestApks.kt index 8e88f5a53..271fc51b7 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/PrintTestApks.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/PrintTestApks.kt @@ -33,6 +33,7 @@ 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.kotlin.dsl.assign import org.gradle.work.DisableCachingByDefault import java.io.File @@ -53,12 +54,12 @@ internal fun Project.configurePrintApksTask(extension: AndroidComponentsExtensio if (artifact != null && testSources != null) { tasks.register( "${variant.name}PrintTestApk", - PrintApkLocationTask::class.java + PrintApkLocationTask::class.java, ) { - apkFolder.set(artifact) - builtArtifactsLoader.set(loader) - variantName.set(variant.name) - sources.set(testSources) + apkFolder = artifact + builtArtifactsLoader = loader + variantName = variant.name + sources = testSources } } } @@ -100,4 +101,4 @@ internal abstract class PrintApkLocationTask : DefaultTask() { val apk = File(builtArtifacts.elements.single().outputFile).toPath() println(apk) } -} \ No newline at end of file +} diff --git a/build.gradle.kts b/build.gradle.kts index 1790cd202..dffc0c0dd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,6 +36,7 @@ plugins { alias(libs.plugins.android.library) apply false alias(libs.plugins.android.test) apply false alias(libs.plugins.baselineprofile) apply false + alias(libs.plugins.compose) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.dependencyGuard) apply false @@ -49,13 +50,3 @@ plugins { alias(libs.plugins.room) apply false alias(libs.plugins.module.graph) apply true // Plugin applied to allow module graph generation } - -// Task to print all the module paths in the project e.g. :core:data -// Used by module graph generator script -tasks.register("printModulePaths") { - subprojects { - if (subprojects.size == 0) { - println(this.path) - } - } -} \ No newline at end of file diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index 3810080f0..31635865c 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -34,6 +34,8 @@ dependencies { api(libs.androidx.compose.foundation.layout) api(libs.androidx.compose.material.iconsExtended) api(libs.androidx.compose.material3) + api(libs.androidx.compose.material3.adaptive) + api(libs.androidx.compose.material3.navigationSuite) api(libs.androidx.compose.runtime) api(libs.androidx.compose.ui.util) diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Navigation.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Navigation.kt index 59f4f48a2..4ac19b482 100644 --- a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Navigation.kt +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/Navigation.kt @@ -23,10 +23,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.NavigationRail import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.NavigationRailItemDefaults import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.WindowAdaptiveInfo +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.NavigationSuiteItemColors +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -165,6 +175,101 @@ fun NiaNavigationRail( ) } +/** + * Now in Android navigation suite scaffold with item and content slots. + * Wraps Material 3 [NavigationSuiteScaffold]. + * + * @param modifier Modifier to be applied to the navigation suite scaffold. + * @param navigationSuiteItems A slot to display multiple items via [NiaNavigationSuiteScope]. + * @param windowAdaptiveInfo The window adaptive info. + * @param content The app content inside the scaffold. + */ +@OptIn( + ExperimentalMaterial3AdaptiveNavigationSuiteApi::class, + ExperimentalMaterial3AdaptiveApi::class, +) +@Composable +fun NiaNavigationSuiteScaffold( + navigationSuiteItems: NiaNavigationSuiteScope.() -> Unit, + modifier: Modifier = Modifier, + windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), + content: @Composable () -> Unit, +) { + val layoutType = NavigationSuiteScaffoldDefaults + .calculateFromAdaptiveInfo(windowAdaptiveInfo) + val navigationSuiteItemColors = NavigationSuiteItemColors( + navigationBarItemColors = NavigationBarItemDefaults.colors( + selectedIconColor = NiaNavigationDefaults.navigationSelectedItemColor(), + unselectedIconColor = NiaNavigationDefaults.navigationContentColor(), + selectedTextColor = NiaNavigationDefaults.navigationSelectedItemColor(), + unselectedTextColor = NiaNavigationDefaults.navigationContentColor(), + indicatorColor = NiaNavigationDefaults.navigationIndicatorColor(), + ), + navigationRailItemColors = NavigationRailItemDefaults.colors( + selectedIconColor = NiaNavigationDefaults.navigationSelectedItemColor(), + unselectedIconColor = NiaNavigationDefaults.navigationContentColor(), + selectedTextColor = NiaNavigationDefaults.navigationSelectedItemColor(), + unselectedTextColor = NiaNavigationDefaults.navigationContentColor(), + indicatorColor = NiaNavigationDefaults.navigationIndicatorColor(), + ), + navigationDrawerItemColors = NavigationDrawerItemDefaults.colors( + selectedIconColor = NiaNavigationDefaults.navigationSelectedItemColor(), + unselectedIconColor = NiaNavigationDefaults.navigationContentColor(), + selectedTextColor = NiaNavigationDefaults.navigationSelectedItemColor(), + unselectedTextColor = NiaNavigationDefaults.navigationContentColor(), + ), + ) + + NavigationSuiteScaffold( + navigationSuiteItems = { + NiaNavigationSuiteScope( + navigationSuiteScope = this, + navigationSuiteItemColors = navigationSuiteItemColors, + ).run(navigationSuiteItems) + }, + layoutType = layoutType, + containerColor = Color.Transparent, + navigationSuiteColors = NavigationSuiteDefaults.colors( + navigationBarContentColor = NiaNavigationDefaults.navigationContentColor(), + navigationRailContainerColor = Color.Transparent, + ), + modifier = modifier, + ) { + content() + } +} + +/** + * A wrapper around [NavigationSuiteScope] to declare navigation items. + */ +@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class) +class NiaNavigationSuiteScope internal constructor( + private val navigationSuiteScope: NavigationSuiteScope, + private val navigationSuiteItemColors: NavigationSuiteItemColors, +) { + fun item( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + icon: @Composable () -> Unit, + selectedIcon: @Composable () -> Unit = icon, + label: @Composable (() -> Unit)? = null, + ) = navigationSuiteScope.item( + selected = selected, + onClick = onClick, + icon = { + if (selected) { + selectedIcon() + } else { + icon() + } + }, + label = label, + colors = navigationSuiteItemColors, + modifier = modifier, + ) +} + @ThemePreviews @Composable fun NiaNavigationBarPreview() { diff --git a/core/designsystem/src/test/screenshots/LoadingWheel/LoadingWheel_animation_20.png b/core/designsystem/src/test/screenshots/LoadingWheel/LoadingWheel_animation_20.png index 74309056f..570474cc1 100644 Binary files a/core/designsystem/src/test/screenshots/LoadingWheel/LoadingWheel_animation_20.png and b/core/designsystem/src/test/screenshots/LoadingWheel/LoadingWheel_animation_20.png differ diff --git a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_androidTheme_notDynamic.png b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_androidTheme_notDynamic.png index ac0065cc7..022ea15eb 100644 Binary files a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_androidTheme_notDynamic.png and b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_androidTheme_notDynamic.png differ diff --git a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_defaultTheme_dynamic.png b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_defaultTheme_dynamic.png index 0133bc71a..0a7be72c2 100644 Binary files a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_defaultTheme_dynamic.png and b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_defaultTheme_dynamic.png differ diff --git a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_defaultTheme_notDynamic.png b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_defaultTheme_notDynamic.png index 2f1d9767c..ddc43ab6a 100644 Binary files a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_defaultTheme_notDynamic.png and b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_dark_defaultTheme_notDynamic.png differ diff --git a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_androidTheme_notDynamic.png b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_androidTheme_notDynamic.png index d90547cd8..071ab0a04 100644 Binary files a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_androidTheme_notDynamic.png and b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_androidTheme_notDynamic.png differ diff --git a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_defaultTheme_dynamic.png b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_defaultTheme_dynamic.png index 142051d68..7170dec31 100644 Binary files a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_defaultTheme_dynamic.png and b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_defaultTheme_dynamic.png differ diff --git a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_defaultTheme_notDynamic.png b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_defaultTheme_notDynamic.png index 6949a8908..6829b0f78 100644 Binary files a/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_defaultTheme_notDynamic.png and b/core/designsystem/src/test/screenshots/LoadingWheel/OverlayLoadingWheel_light_defaultTheme_notDynamic.png differ diff --git a/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_androidTheme_notDynamic.png b/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_androidTheme_notDynamic.png index 4d4b10caa..b2a0fb99c 100644 Binary files a/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_androidTheme_notDynamic.png and b/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_androidTheme_notDynamic.png differ diff --git a/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_defaultTheme_dynamic.png b/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_defaultTheme_dynamic.png index 589628199..8836faebc 100644 Binary files a/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_defaultTheme_dynamic.png and b/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_defaultTheme_dynamic.png differ diff --git a/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_defaultTheme_notDynamic.png b/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_defaultTheme_notDynamic.png index bfa5a8367..a4abd2d5b 100644 Binary files a/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_defaultTheme_notDynamic.png and b/core/designsystem/src/test/screenshots/Navigation/Navigation_dark_defaultTheme_notDynamic.png differ diff --git a/core/designsystem/src/test/screenshots/Navigation/Navigation_fontScale2.png b/core/designsystem/src/test/screenshots/Navigation/Navigation_fontScale2.png index 6e951e42f..97bbb0892 100644 Binary files a/core/designsystem/src/test/screenshots/Navigation/Navigation_fontScale2.png and b/core/designsystem/src/test/screenshots/Navigation/Navigation_fontScale2.png differ diff --git a/core/designsystem/src/test/screenshots/Navigation/Navigation_light_androidTheme_notDynamic.png b/core/designsystem/src/test/screenshots/Navigation/Navigation_light_androidTheme_notDynamic.png index 3c1ab3d40..a526e36c7 100644 Binary files a/core/designsystem/src/test/screenshots/Navigation/Navigation_light_androidTheme_notDynamic.png and b/core/designsystem/src/test/screenshots/Navigation/Navigation_light_androidTheme_notDynamic.png differ diff --git a/core/designsystem/src/test/screenshots/Navigation/Navigation_light_defaultTheme_dynamic.png b/core/designsystem/src/test/screenshots/Navigation/Navigation_light_defaultTheme_dynamic.png index 391de3204..5e27d2497 100644 Binary files a/core/designsystem/src/test/screenshots/Navigation/Navigation_light_defaultTheme_dynamic.png and b/core/designsystem/src/test/screenshots/Navigation/Navigation_light_defaultTheme_dynamic.png differ diff --git a/core/designsystem/src/test/screenshots/Navigation/Navigation_light_defaultTheme_notDynamic.png b/core/designsystem/src/test/screenshots/Navigation/Navigation_light_defaultTheme_notDynamic.png index 6a342f7bd..f5671cb14 100644 Binary files a/core/designsystem/src/test/screenshots/Navigation/Navigation_light_defaultTheme_notDynamic.png and b/core/designsystem/src/test/screenshots/Navigation/Navigation_light_defaultTheme_notDynamic.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenLoading_foldable.png b/feature/foryou/src/test/screenshots/ForYouScreenLoading_foldable.png index 92d2978e0..f362c445d 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenLoading_foldable.png and b/feature/foryou/src/test/screenshots/ForYouScreenLoading_foldable.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenLoading_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenLoading_phone.png index 0e6aedd53..8d02e5985 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenLoading_phone.png and b/feature/foryou/src/test/screenshots/ForYouScreenLoading_phone.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenLoading_tablet.png b/feature/foryou/src/test/screenshots/ForYouScreenLoading_tablet.png index 88b6ce240..e6f6a527a 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenLoading_tablet.png and b/feature/foryou/src/test/screenshots/ForYouScreenLoading_tablet.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png index 1972b1ca2..f5ca39c3a 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png index 16df589f9..7a3f99d7c 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png index d28704e49..3a14048b5 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png index c2a01f2d8..97458f73b 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png differ diff --git a/generateModuleGraphs.sh b/generateModuleGraphs.sh index 3a826586b..3c3583e67 100755 --- a/generateModuleGraphs.sh +++ b/generateModuleGraphs.sh @@ -31,6 +31,27 @@ then exit 1 fi +# Check if the svgo command is available +if ! command -v svgo &> /dev/null +then + echo "The 'svgo' command is not found. This is required to cleanup and compress SVGs." + echo "Installation instructions available at https://github.com/svg/svgo." + exit 1 +fi + +# Check for a version of grep which supports Perl regex. +# On MacOS the OS installed grep doesn't support Perl regex so check for the existence of the +# GNU version instead which is prefixed with 'g' to distinguish it from the OS installed version. + if grep -P "" /dev/null > /dev/null 2>&1; then + GREP_COMMAND=grep +elif command -v ggrep &> /dev/null; then + GREP_COMMAND=ggrep +else + echo "You don't have a version of 'grep' installed which supports Perl regular expressions." + echo "On MacOS you can install one using Homebrew with the command: 'brew install grep'" + exit 1 +fi + # Initialize an array to store excluded modules excluded_modules=() @@ -50,7 +71,7 @@ while [[ $# -gt 0 ]]; do done # Get the module paths -module_paths=$(./gradlew -q printModulePaths --no-configuration-cache) +module_paths=$(${GREP_COMMAND} -oP 'include\("\K[^"]+' settings.gradle.kts) # Ensure the output directory exists mkdir -p docs/images/graphs/ @@ -100,12 +121,10 @@ echo "$module_paths" | while read -r module_path; do -Pmodules.graph.output.gv="/tmp/${file_name}.gv" \ -Pmodules.graph.of.module="${module_path}" /-->\x0/g' | grep -zv '^