diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 5bf03b47a..64d22fd5e 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -35,7 +35,7 @@ jobs: java-version: 17 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 with: validate-wrappers: true gradle-home-cache-cleanup: true @@ -187,7 +187,7 @@ jobs: java-version: 17 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 with: validate-wrappers: true gradle-home-cache-cleanup: true @@ -222,7 +222,7 @@ jobs: - name: Display local test coverage (only API 30) if: matrix.api-level == 30 id: jacoco - uses: madrapps/jacoco-report@v1.6.1 + uses: madrapps/jacoco-report@v1.7.0 with: title: Combined test coverage report min-coverage-overall: 40 diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index b18b41faa..56119f5bd 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -32,7 +32,7 @@ jobs: java-version: 17 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 with: validate-wrappers: true gradle-home-cache-cleanup: true diff --git a/README.md b/README.md index be1270b16..1f5270323 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,8 @@ Examples: To run the tests execute the following gradle tasks: -- `testDemoDebug` run all local tests against the `demoDebug` variant. +- `testDemoDebug` run all local tests against the `demoDebug` variant. Screenshot tests will fail +(see below for explanation). To avoid this, run `recordRoborazziDemoDebug` prior to running unit tests. - `connectedDemoDebugAndroidTest` run all instrumented tests against the `demoDebug` variant. **Note:** You should not run `./gradlew test` or `./gradlew connectedAndroidTest` as this will execute @@ -137,7 +138,7 @@ stored in `modulename/src/test/screenshots`. - `compareRoborazziDemoDebug` create comparison images between failed tests and the known correct images. These can also be found in `modulename/src/test/screenshots`. -**Note:** The known correct screenshots stored in this repository are recorded on CI using Linux. Other +**Note on failing screenshot tests:** The known correct screenshots stored in this repository are recorded on CI using Linux. Other platforms may (and probably will) generate slightly different images, making the screenshot tests fail. When working on a non-Linux platform, a workaround to this is to run `recordRoborazziDemoDebug` on the `main` branch before starting work. After making changes, `verifyRoborazziDemoDebug` will identify only diff --git a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt index b9135ed42..f95d9ed16 100644 --- a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt +++ b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt @@ -12,45 +12,45 @@ 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-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.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.compose.animation:animation-android:1.7.0 +androidx.compose.animation:animation-core-android:1.7.0 +androidx.compose.animation:animation-core:1.7.0 +androidx.compose.animation:animation:1.7.0 +androidx.compose.foundation:foundation-android:1.7.0 +androidx.compose.foundation:foundation-layout-android:1.7.0 +androidx.compose.foundation:foundation-layout:1.7.0 +androidx.compose.foundation:foundation:1.7.0 +androidx.compose.material3.adaptive:adaptive-android:1.0.0 +androidx.compose.material3.adaptive:adaptive:1.0.0 +androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.0 +androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0 +androidx.compose.material3:material3-android:1.3.0 +androidx.compose.material3:material3:1.3.0 +androidx.compose.material:material-icons-core-android:1.7.0 +androidx.compose.material:material-icons-core:1.7.0 +androidx.compose.material:material-icons-extended-android:1.7.0 +androidx.compose.material:material-icons-extended:1.7.0 +androidx.compose.material:material-ripple-android:1.7.0 +androidx.compose.material:material-ripple:1.7.0 +androidx.compose.runtime:runtime-android:1.7.0 +androidx.compose.runtime:runtime-saveable-android:1.7.0 +androidx.compose.runtime:runtime-saveable:1.7.0 +androidx.compose.runtime:runtime:1.7.0 +androidx.compose.ui:ui-android:1.7.0 +androidx.compose.ui:ui-geometry-android:1.7.0 +androidx.compose.ui:ui-geometry:1.7.0 +androidx.compose.ui:ui-graphics-android:1.7.0 +androidx.compose.ui:ui-graphics:1.7.0 +androidx.compose.ui:ui-text-android:1.7.0 +androidx.compose.ui:ui-text:1.7.0 +androidx.compose.ui:ui-tooling-preview-android:1.7.0 +androidx.compose.ui:ui-tooling-preview:1.7.0 +androidx.compose.ui:ui-unit-android:1.7.0 +androidx.compose.ui:ui-unit:1.7.0 +androidx.compose.ui:ui-util-android:1.7.0 +androidx.compose.ui:ui-util:1.7.0 +androidx.compose.ui:ui:1.7.0 +androidx.compose:compose-bom:2024.09.00 androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.13.1 androidx.core:core:1.13.1 @@ -61,23 +61,23 @@ 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.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.lifecycle:lifecycle-common-java8:2.8.3 +androidx.lifecycle:lifecycle-common-jvm:2.8.3 +androidx.lifecycle:lifecycle-common:2.8.3 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.3 +androidx.lifecycle:lifecycle-livedata-core:2.8.3 +androidx.lifecycle:lifecycle-livedata:2.8.3 +androidx.lifecycle:lifecycle-process:2.8.3 +androidx.lifecycle:lifecycle-runtime-android:2.8.3 +androidx.lifecycle:lifecycle-runtime-compose-android:2.8.3 +androidx.lifecycle:lifecycle-runtime-compose:2.8.3 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.3 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.3 +androidx.lifecycle:lifecycle-runtime:2.8.3 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.3 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.3 +androidx.lifecycle:lifecycle-viewmodel:2.8.3 androidx.loader:loader:1.0.0 androidx.metrics:metrics-performance:1.0.0-alpha04 androidx.profileinstaller:profileinstaller:1.3.1 @@ -91,9 +91,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-beta02 -androidx.window:window-core:1.3.0-beta02 -androidx.window:window:1.3.0-beta02 +androidx.window:window-core-android:1.3.0 +androidx.window:window-core:1.3.0 +androidx.window:window:1.3.0 com.google.accompanist:accompanist-drawablepainter:0.32.0 com.google.code.findbugs:jsr305:3.0.2 com.google.dagger:dagger-lint-aar:2.51.1 @@ -109,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:2.0.0 +org.jetbrains.kotlin:kotlin-stdlib-common:2.0.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 -org.jetbrains.kotlin:kotlin-stdlib:2.0.0 +org.jetbrains.kotlin:kotlin-stdlib:2.0.20 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/build.gradle.kts b/app/build.gradle.kts index 9c6989f67..1abc44bac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,6 +25,7 @@ plugins { id("com.google.android.gms.oss-licenses-plugin") alias(libs.plugins.baselineprofile) alias(libs.plugins.roborazzi) + alias(libs.plugins.kotlin.serialization) } android { @@ -103,6 +104,7 @@ dependencies { implementation(libs.androidx.window.core) implementation(libs.kotlinx.coroutines.guava) implementation(libs.coil.kt) + implementation(libs.kotlinx.serialization.json) ksp(libs.hilt.compiler) @@ -114,6 +116,7 @@ dependencies { testImplementation(projects.core.dataTest) testImplementation(libs.hilt.android.testing) testImplementation(projects.sync.syncTest) + testImplementation(libs.kotlin.test) testDemoImplementation(libs.robolectric) testDemoImplementation(libs.roborazzi) diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt index 96e5940e8..9eec115fa 100644 --- a/app/dependencies/prodReleaseRuntimeClasspath.txt +++ b/app/dependencies/prodReleaseRuntimeClasspath.txt @@ -1,64 +1,64 @@ 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 -androidx.annotation:annotation:1.8.0 +androidx.annotation:annotation-experimental:1.4.1 +androidx.annotation:annotation-jvm:1.8.1 +androidx.annotation:annotation:1.8.1 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 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-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.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.collection:collection-jvm:1.4.2 +androidx.collection:collection-ktx:1.4.2 +androidx.collection:collection:1.4.2 +androidx.compose.animation:animation-android:1.7.0 +androidx.compose.animation:animation-core-android:1.7.0 +androidx.compose.animation:animation-core:1.7.0 +androidx.compose.animation:animation:1.7.0 +androidx.compose.foundation:foundation-android:1.7.0 +androidx.compose.foundation:foundation-layout-android:1.7.0 +androidx.compose.foundation:foundation-layout:1.7.0 +androidx.compose.foundation:foundation:1.7.0 +androidx.compose.material3.adaptive:adaptive-android:1.0.0 +androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0 +androidx.compose.material3.adaptive:adaptive-layout:1.0.0 +androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0 +androidx.compose.material3.adaptive:adaptive-navigation:1.0.0 +androidx.compose.material3.adaptive:adaptive:1.0.0 +androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.0 +androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0 +androidx.compose.material3:material3-android:1.3.0 +androidx.compose.material3:material3-window-size-class-android:1.3.0 +androidx.compose.material3:material3-window-size-class:1.3.0 +androidx.compose.material3:material3:1.3.0 +androidx.compose.material:material-icons-core-android:1.7.0 +androidx.compose.material:material-icons-core:1.7.0 +androidx.compose.material:material-icons-extended-android:1.7.0 +androidx.compose.material:material-icons-extended:1.7.0 +androidx.compose.material:material-ripple-android:1.7.0 +androidx.compose.material:material-ripple:1.7.0 +androidx.compose.runtime:runtime-android:1.7.0 +androidx.compose.runtime:runtime-saveable-android:1.7.0 +androidx.compose.runtime:runtime-saveable:1.7.0 androidx.compose.runtime:runtime-tracing:1.0.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.compose.runtime:runtime:1.7.0 +androidx.compose.ui:ui-android:1.7.0 +androidx.compose.ui:ui-geometry-android:1.7.0 +androidx.compose.ui:ui-geometry:1.7.0 +androidx.compose.ui:ui-graphics-android:1.7.0 +androidx.compose.ui:ui-graphics:1.7.0 +androidx.compose.ui:ui-text-android:1.7.0 +androidx.compose.ui:ui-text:1.7.0 +androidx.compose.ui:ui-tooling-preview-android:1.7.0 +androidx.compose.ui:ui-tooling-preview:1.7.0 +androidx.compose.ui:ui-unit-android:1.7.0 +androidx.compose.ui:ui-unit:1.7.0 +androidx.compose.ui:ui-util-android:1.7.0 +androidx.compose.ui:ui-util:1.7.0 +androidx.compose.ui:ui:1.7.0 +androidx.compose:compose-bom:2024.09.00 androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.13.1 androidx.core:core-splashscreen:1.0.1 @@ -106,11 +106,11 @@ androidx.lifecycle:lifecycle-viewmodel:2.8.3 androidx.loader:loader:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.metrics:metrics-performance:1.0.0-alpha04 -androidx.navigation:navigation-common-ktx:2.8.0-alpha06 -androidx.navigation:navigation-common:2.8.0-alpha06 -androidx.navigation:navigation-compose:2.8.0-alpha06 -androidx.navigation:navigation-runtime-ktx:2.8.0-alpha06 -androidx.navigation:navigation-runtime:2.8.0-alpha06 +androidx.navigation:navigation-common-ktx:2.8.0 +androidx.navigation:navigation-common:2.8.0 +androidx.navigation:navigation-compose:2.8.0 +androidx.navigation:navigation-runtime-ktx:2.8.0 +androidx.navigation:navigation-runtime:2.8.0 androidx.print:print:1.0.0 androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05 androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05 @@ -132,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-beta02 -androidx.window:window-core:1.3.0-beta02 -androidx.window:window:1.3.0-beta02 +androidx.window:window-core-android:1.3.0 +androidx.window:window-core:1.3.0 +androidx.window:window:1.3.0 androidx.work:work-runtime-ktx:2.9.0 androidx.work:work-runtime:2.9.0 com.caverock:androidsvg-aar:1.4 @@ -203,10 +203,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:2.0.0 +org.jetbrains.kotlin:kotlin-stdlib-common:2.0.20 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 -org.jetbrains.kotlin:kotlin-stdlib:2.0.0 +org.jetbrains.kotlin:kotlin-stdlib:2.0.20 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/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 716305ab6..5b22f9865 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,9 +47,13 @@ - + + + + + + + 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 2f8572102..cffb13f34 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,7 +23,6 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect @@ -55,8 +54,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject -private const val TAG = "MainActivity" - @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -148,7 +145,6 @@ 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/navigation/NiaNavHost.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt index 39bc03de7..f878c003b 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt @@ -20,7 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.FOR_YOU_ROUTE +import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen @@ -40,12 +40,11 @@ fun NiaNavHost( appState: NiaAppState, onShowSnackbar: suspend (String, String?) -> Boolean, modifier: Modifier = Modifier, - startDestination: String = FOR_YOU_ROUTE, ) { val navController = appState.navController NavHost( navController = navController, - startDestination = startDestination, + startDestination = ForYouRoute, modifier = modifier, ) { forYouScreen(onTopicClick = navController::navigateToInterests) diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt index aca7d54ab..815061273 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt @@ -16,9 +16,14 @@ package com.google.samples.apps.nowinandroid.navigation +import androidx.annotation.StringRes import androidx.compose.ui.graphics.vector.ImageVector import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons +import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksRoute +import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute +import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute +import kotlin.reflect.KClass import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR import com.google.samples.apps.nowinandroid.feature.search.R as searchR @@ -31,25 +36,29 @@ import com.google.samples.apps.nowinandroid.feature.search.R as searchR enum class TopLevelDestination( val selectedIcon: ImageVector, val unselectedIcon: ImageVector, - val iconTextId: Int, - val titleTextId: Int, + @StringRes val iconTextId: Int, + @StringRes val titleTextId: Int, + val route: KClass<*>, ) { FOR_YOU( selectedIcon = NiaIcons.Upcoming, unselectedIcon = NiaIcons.UpcomingBorder, iconTextId = forYouR.string.feature_foryou_title, titleTextId = R.string.app_name, + route = ForYouRoute::class, ), BOOKMARKS( selectedIcon = NiaIcons.Bookmarks, unselectedIcon = NiaIcons.BookmarksBorder, iconTextId = bookmarksR.string.feature_bookmarks_title, titleTextId = bookmarksR.string.feature_bookmarks_title, + route = BookmarksRoute::class, ), INTERESTS( selectedIcon = NiaIcons.Grid3x3, unselectedIcon = NiaIcons.Grid3x3, iconTextId = searchR.string.feature_search_interests, titleTextId = searchR.string.feature_search_interests, + route = InterestsRoute::class, ), } 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 b47984ddb..6cdc32bb0 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 @@ -60,6 +60,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavDestination.Companion.hierarchy import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground @@ -72,6 +73,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradien import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination +import kotlin.reflect.KClass import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR @OptIn(ExperimentalMaterial3AdaptiveApi::class) @@ -150,7 +152,7 @@ internal fun NiaApp( appState.topLevelDestinations.forEach { destination -> val hasUnread = unreadDestinations.contains(destination) val selected = currentDestination - .isTopLevelDestinationInHierarchy(destination) + .isRouteInHierarchy(destination.route) item( selected = selected, onClick = { appState.navigateToTopLevelDestination(destination) }, @@ -198,8 +200,10 @@ internal fun NiaApp( ) { // Show the top app bar on top level destinations. val destination = appState.currentTopLevelDestination - val shouldShowTopAppBar = destination != null + var shouldShowTopAppBar = false + if (destination != null) { + shouldShowTopAppBar = true NiaTopAppBar( titleRes = destination.titleTextId, navigationIcon = NiaIcons.Search, @@ -266,7 +270,7 @@ private fun Modifier.notificationDot(): Modifier = } } -private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) = +private fun NavDestination?.isRouteInHierarchy(route: KClass<*>) = this?.hierarchy?.any { - it.route?.contains(destination.name, true) ?: false + it.hasRoute(route) } ?: false 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 519603579..75a294c01 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 @@ -22,6 +22,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.NavController import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState @@ -32,11 +33,8 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourc 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.ui.TrackDisposableJank -import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BOOKMARKS_ROUTE import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.FOR_YOU_ROUTE import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou -import com.google.samples.apps.nowinandroid.feature.interests.navigation.INTERESTS_ROUTE import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests import com.google.samples.apps.nowinandroid.feature.search.navigation.navigateToSearch import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination @@ -90,11 +88,10 @@ class NiaAppState( .currentBackStackEntryAsState().value?.destination val currentTopLevelDestination: TopLevelDestination? - @Composable get() = when (currentDestination?.route) { - FOR_YOU_ROUTE -> FOR_YOU - BOOKMARKS_ROUTE -> BOOKMARKS - INTERESTS_ROUTE -> INTERESTS - else -> null + @Composable get() { + return TopLevelDestination.entries.firstOrNull { topLevelDestination -> + currentDestination?.hasRoute(route = topLevelDestination.route) ?: false + } } val isOffline = networkMonitor.isOnline diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt index 40ce9c116..3d37f3417 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt @@ -18,19 +18,26 @@ package com.google.samples.apps.nowinandroid.ui.interests2pane import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG +import androidx.navigation.toRoute +import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject +const val TOPIC_ID_KEY = "selectedTopicId" + @HiltViewModel class Interests2PaneViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, ) : ViewModel() { - val selectedTopicId: StateFlow = - savedStateHandle.getStateFlow(TOPIC_ID_ARG, savedStateHandle[TOPIC_ID_ARG]) + + val route = savedStateHandle.toRoute() + val selectedTopicId: StateFlow = savedStateHandle.getStateFlow( + key = TOPIC_ID_KEY, + initialValue = route.initialTopicId, + ) fun onTopicClick(topicId: String?) { - savedStateHandle[TOPIC_ID_ARG] = topicId + savedStateHandle[TOPIC_ID_KEY] = topicId } } diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt index 919cb44f2..669c6300a 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.nowinandroid.ui.interests2pane import androidx.activity.compose.BackHandler +import androidx.annotation.Keep import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -39,34 +40,26 @@ import androidx.compose.runtime.setValue import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute -import com.google.samples.apps.nowinandroid.feature.interests.navigation.INTERESTS_ROUTE -import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG +import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder -import com.google.samples.apps.nowinandroid.feature.topic.navigation.TOPIC_ROUTE -import com.google.samples.apps.nowinandroid.feature.topic.navigation.createTopicRoute +import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen +import kotlinx.serialization.Serializable import java.util.UUID -private const val DETAIL_PANE_NAVHOST_ROUTE = "detail_pane_route" +@Serializable internal object TopicPlaceholderRoute + +// TODO: Remove @Keep when https://issuetracker.google.com/353898971 is fixed +@Keep +@Serializable internal object DetailPaneNavHostRoute fun NavGraphBuilder.interestsListDetailScreen() { - composable( - route = INTERESTS_ROUTE, - arguments = listOf( - navArgument(TOPIC_ID_ARG) { - type = NavType.StringType - defaultValue = null - nullable = true - }, - ), - ) { + composable { InterestsListDetailScreen() } } @@ -104,8 +97,9 @@ internal fun InterestsListDetailScreen( listDetailNavigator.navigateBack() } - var nestedNavHostStartDestination by remember { - mutableStateOf(selectedTopicId?.let(::createTopicRoute) ?: TOPIC_ROUTE) + var nestedNavHostStartRoute by remember { + val route = selectedTopicId?.let { TopicRoute(id = it) } ?: TopicPlaceholderRoute + mutableStateOf(route) } var nestedNavKey by rememberSaveable( stateSaver = Saver({ it.toString() }, UUID::fromString), @@ -122,11 +116,11 @@ internal fun InterestsListDetailScreen( // If the detail pane was visible, then use the nestedNavController navigate call // directly nestedNavController.navigateToTopic(topicId) { - popUpTo(DETAIL_PANE_NAVHOST_ROUTE) + popUpTo() } } else { // Otherwise, recreate the NavHost entirely, and start at the new destination - nestedNavHostStartDestination = createTopicRoute(topicId) + nestedNavHostStartRoute = TopicRoute(id = topicId) nestedNavKey = UUID.randomUUID() } listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail) @@ -148,15 +142,15 @@ internal fun InterestsListDetailScreen( key(nestedNavKey) { NavHost( navController = nestedNavController, - startDestination = nestedNavHostStartDestination, - route = DETAIL_PANE_NAVHOST_ROUTE, + startDestination = nestedNavHostStartRoute, + route = DetailPaneNavHostRoute::class, ) { topicScreen( showBackButton = !listDetailNavigator.isListPaneVisible(), onBackClick = listDetailNavigator::navigateBack, onTopicClick = ::onTopicClickShowDetailPane, ) - composable(route = TOPIC_ROUTE) { + composable { TopicDetailPlaceholder() } } 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 index 2fc88e561..a2409dd89 100644 --- 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 @@ -17,18 +17,17 @@ package com.google.samples.apps.nowinandroid.ui import android.view.WindowInsets -import android.widget.FrameLayout +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.AbstractComposeView 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. + * A [DeviceConfigurationOverride] that overrides the window insets for the contained content. */ @Suppress("ktlint:standard:function-naming") fun DeviceConfigurationOverride.Companion.WindowInsets( @@ -38,10 +37,17 @@ fun DeviceConfigurationOverride.Companion.WindowInsets( val currentWindowInsets by rememberUpdatedState(windowInsets) AndroidView( factory = { context -> - object : FrameLayout(context) { + object : AbstractComposeView(context) { + @Composable + override fun Content() { + currentContentUnderTest() + } + override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { children.forEach { - it.dispatchApplyWindowInsets(currentWindowInsets.toWindowInsets()) + it.dispatchApplyWindowInsets( + WindowInsets(currentWindowInsets.toWindowInsets()), + ) } return WindowInsetsCompat.CONSUMED.toWindowInsets()!! } @@ -52,17 +58,10 @@ fun DeviceConfigurationOverride.Companion.WindowInsets( */ @Deprecated("Deprecated in Java") override fun requestFitSystemWindows() { - dispatchApplyWindowInsets(currentWindowInsets.toWindowInsets()!!) + dispatchApplyWindowInsets(WindowInsets(currentWindowInsets.toWindowInsets()!!)) } - }.apply { - addView( - ComposeView(context).apply { - setContent { - currentContentUnderTest() - } - }, - ) } }, + update = { with(currentWindowInsets) { it.requestApplyInsets() } }, ) } diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreenTest.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/InterestsListDetailScreenTest.kt similarity index 63% rename from app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreenTest.kt rename to app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/InterestsListDetailScreenTest.kt index 21ac3e920..a5b243537 100644 --- a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreenTest.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/InterestsListDetailScreenTest.kt @@ -14,43 +14,49 @@ * limitations under the License. */ -package com.google.samples.apps.nowinandroid.ui.interests2pane +package com.google.samples.apps.nowinandroid.ui import androidx.activity.compose.BackHandler -import androidx.compose.material3.adaptive.Posture -import androidx.compose.material3.adaptive.WindowAdaptiveInfo -import androidx.compose.ui.test.DeviceConfigurationOverride -import androidx.compose.ui.test.ForcedSize +import androidx.annotation.StringRes import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp import androidx.test.espresso.Espresso -import androidx.window.core.layout.WindowSizeClass import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.Topic -import com.google.samples.apps.nowinandroid.ui.stringResource +import com.google.samples.apps.nowinandroid.ui.interests2pane.InterestsListDetailScreen 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.flow.first 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 javax.inject.Inject +import kotlin.properties.ReadOnlyProperty import kotlin.test.assertTrue import com.google.samples.apps.nowinandroid.feature.topic.R as FeatureTopicR +private const val EXPANDED_WIDTH = "w1200dp-h840dp" +private const val COMPACT_WIDTH = "w412dp-h915dp" + @HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) class InterestsListDetailScreenTest { + @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @@ -64,6 +70,11 @@ class InterestsListDetailScreenTest { @Inject lateinit var topicsRepository: TopicsRepository + /** Convenience function for getting all topics during tests, */ + private fun getTopics(): List = runBlocking { + topicsRepository.getTopics().first().sortedBy { it.name } + } + // The strings used for matching in these tests. private val placeholderText by composeTestRule.stringResource(FeatureTopicR.string.feature_topic_select_an_interest) private val listPaneTag = "interests:topics" @@ -71,39 +82,18 @@ class InterestsListDetailScreenTest { private val Topic.testTag get() = "topic:${this.id}" - // Overrides for device sizes. - private enum class TestDeviceConfig(widthDp: Float, heightDp: Float) { - Compact(412f, 915f), - Expanded(1200f, 840f), - ; - - val sizeOverride = DeviceConfigurationOverride.ForcedSize(DpSize(widthDp.dp, heightDp.dp)) - val adaptiveInfo = WindowAdaptiveInfo( - windowSizeClass = WindowSizeClass.compute(widthDp, heightDp), - windowPosture = Posture(), - ) - } - @Before fun setup() { hiltRule.inject() } - /** Convenience function for getting all topics during tests, */ - private fun getTopics(): List = runBlocking { - topicsRepository.getTopics().first() - } - @Test + @Config(qualifiers = EXPANDED_WIDTH) fun expandedWidth_initialState_showsTwoPanesWithPlaceholder() { composeTestRule.apply { setContent { - with(TestDeviceConfig.Expanded) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } - } + NiaTheme { + InterestsListDetailScreen() } } @@ -113,15 +103,12 @@ class InterestsListDetailScreenTest { } @Test + @Config(qualifiers = COMPACT_WIDTH) fun compactWidth_initialState_showsListPane() { composeTestRule.apply { setContent { - with(TestDeviceConfig.Compact) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } - } + NiaTheme { + InterestsListDetailScreen() } } @@ -131,15 +118,12 @@ class InterestsListDetailScreenTest { } @Test + @Config(qualifiers = EXPANDED_WIDTH) fun expandedWidth_topicSelected_updatesDetailPane() { composeTestRule.apply { setContent { - with(TestDeviceConfig.Expanded) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } - } + NiaTheme { + InterestsListDetailScreen() } } @@ -153,15 +137,12 @@ class InterestsListDetailScreenTest { } @Test + @Config(qualifiers = COMPACT_WIDTH) fun compactWidth_topicSelected_showsTopicDetailPane() { composeTestRule.apply { setContent { - with(TestDeviceConfig.Compact) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } - } + NiaTheme { + InterestsListDetailScreen() } } @@ -175,27 +156,25 @@ class InterestsListDetailScreenTest { } @Test + @Config(qualifiers = EXPANDED_WIDTH) fun expandedWidth_backPressFromTopicDetail_leavesInterests() { var unhandledBackPress = false composeTestRule.apply { setContent { - with(TestDeviceConfig.Expanded) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - // Back press should not be handled by the two pane layout, and thus - // "fall through" to this BackHandler. - BackHandler { - unhandledBackPress = true - } - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } + NiaTheme { + // Back press should not be handled by the two pane layout, and thus + // "fall through" to this BackHandler. + BackHandler { + unhandledBackPress = true } + InterestsListDetailScreen() } } val firstTopic = getTopics().first() onNodeWithText(firstTopic.name).performClick() + waitForIdle() Espresso.pressBack() assertTrue(unhandledBackPress) @@ -203,21 +182,19 @@ class InterestsListDetailScreenTest { } @Test + @Config(qualifiers = COMPACT_WIDTH) fun compactWidth_backPressFromTopicDetail_showsListPane() { composeTestRule.apply { setContent { - with(TestDeviceConfig.Compact) { - DeviceConfigurationOverride(override = sizeOverride) { - NiaTheme { - InterestsListDetailScreen(windowAdaptiveInfo = adaptiveInfo) - } - } + NiaTheme { + InterestsListDetailScreen() } } val firstTopic = getTopics().first() onNodeWithText(firstTopic.name).performClick() + waitForIdle() Espresso.pressBack() onNodeWithTag(listPaneTag).assertIsDisplayed() @@ -226,3 +203,8 @@ class InterestsListDetailScreenTest { } } } + +private fun AndroidComposeTestRule<*, *>.stringResource( + @StringRes resId: Int, +): ReadOnlyProperty = + ReadOnlyProperty { _, _ -> activity.getString(resId) } diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 9110e7fa3..6d0f213d4 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -28,6 +28,7 @@ class AndroidFeatureConventionPlugin : Plugin { pluginManager.apply { apply("nowinandroid.android.library") apply("nowinandroid.hilt") + apply("org.jetbrains.kotlin.plugin.serialization") } extensions.configure { testOptions.animationsDisabled = true @@ -41,8 +42,11 @@ class AndroidFeatureConventionPlugin : Plugin { add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get()) add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) + add("implementation", libs.findLibrary("androidx.navigation.compose").get()) add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) + add("implementation", libs.findLibrary("kotlinx.serialization.json").get()) + add("testImplementation", libs.findLibrary("androidx.navigation.testing").get()) add("androidTestImplementation", libs.findLibrary("androidx.lifecycle.runtimeTesting").get()) } } 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 f16a8051a..ffb6358c3 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 @@ -65,8 +65,7 @@ internal fun Project.configureAndroidCompose( .relativeToRootProject("compose-reports") .let(reportsDestination::set) - stabilityConfigurationFile = rootProject.layout.projectDirectory.file("compose_compiler_config.conf") - - enableStrongSkippingMode = true + stabilityConfigurationFile = + rootProject.layout.projectDirectory.file("compose_compiler_config.conf") } } 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 4447b8602..59eac2322 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 @@ -35,12 +35,12 @@ import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction -import org.gradle.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 import java.io.File +import java.util.Locale import javax.inject.Inject @CacheableTask @@ -107,6 +107,10 @@ abstract class CheckBadgingTask : DefaultTask() { } } +private fun String.capitalized() = replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() +} + fun Project.configureBadgingTasks( baseExtension: BaseExtension, componentsExtension: ApplicationAndroidComponentsExtension, 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 570474cc1..74309056f 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/model/src/main/kotlin/com/google/samples/apps/nowinandroid/core/model/data/UserNewsResource.kt b/core/model/src/main/kotlin/com/google/samples/apps/nowinandroid/core/model/data/UserNewsResource.kt index a56bbcb8d..77dfa4394 100644 --- a/core/model/src/main/kotlin/com/google/samples/apps/nowinandroid/core/model/data/UserNewsResource.kt +++ b/core/model/src/main/kotlin/com/google/samples/apps/nowinandroid/core/model/data/UserNewsResource.kt @@ -22,6 +22,7 @@ import kotlinx.datetime.Instant * A [NewsResource] with additional user information such as whether the user is following the * news resource's topics and whether they have saved (bookmarked) this news resource. */ +@ConsistentCopyVisibility data class UserNewsResource internal constructor( val id: String, val title: String, diff --git a/core/notifications/src/main/kotlin/com/google/samples/apps/nowinandroid/core/notifications/SystemTrayNotifier.kt b/core/notifications/src/main/kotlin/com/google/samples/apps/nowinandroid/core/notifications/SystemTrayNotifier.kt index 5da88102a..3fc8114dd 100644 --- a/core/notifications/src/main/kotlin/com/google/samples/apps/nowinandroid/core/notifications/SystemTrayNotifier.kt +++ b/core/notifications/src/main/kotlin/com/google/samples/apps/nowinandroid/core/notifications/SystemTrayNotifier.kt @@ -44,7 +44,10 @@ private const val NEWS_NOTIFICATION_SUMMARY_ID = 1 private const val NEWS_NOTIFICATION_CHANNEL_ID = "" private const val NEWS_NOTIFICATION_GROUP = "NEWS_NOTIFICATIONS" private const val DEEP_LINK_SCHEME_AND_HOST = "https://www.nowinandroid.apps.samples.google.com" -private const val FOR_YOU_PATH = "foryou" +private const val DEEP_LINK_FOR_YOU_PATH = "foryou" +private const val DEEP_LINK_BASE_PATH = "$DEEP_LINK_SCHEME_AND_HOST/$DEEP_LINK_FOR_YOU_PATH" +const val DEEP_LINK_NEWS_RESOURCE_ID_KEY = "linkedNewsResourceId" +const val DEEP_LINK_URI_PATTERN = "$DEEP_LINK_BASE_PATH/{$DEEP_LINK_NEWS_RESOURCE_ID_KEY}" /** * Implementation of [Notifier] that displays notifications in the system tray. @@ -161,4 +164,4 @@ private fun Context.newsPendingIntent( PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) -private fun NewsResource.newsDeepLinkUri() = "$DEEP_LINK_SCHEME_AND_HOST/$FOR_YOU_PATH/$id".toUri() +private fun NewsResource.newsDeepLinkUri() = "$DEEP_LINK_BASE_PATH/$id".toUri() diff --git a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt index afdb584a2..c22a02fa1 100644 --- a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt +++ b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt @@ -87,7 +87,7 @@ fun LazyStaggeredGridScope.newsFeed( onTopicClick = onTopicClick, modifier = Modifier .padding(horizontal = 8.dp) - .animateItemPlacement(), + .animateItem(), ) } } diff --git a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index e60c498eb..7c41d74d0 100644 --- a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -16,8 +16,15 @@ package com.google.samples.apps.nowinandroid.core.ui +import android.content.ClipData +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import android.view.View import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.draganddrop.dragAndDropSource +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -45,6 +52,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.DragAndDropTransferData import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalInspectionMode @@ -77,6 +85,7 @@ import java.util.Locale * [NewsResource] card used on the following screens: For You, Saved */ +@OptIn(ExperimentalFoundationApi::class) @Composable fun NewsResourceCardExpanded( userNewsResource: UserNewsResource, @@ -88,6 +97,19 @@ fun NewsResourceCardExpanded( modifier: Modifier = Modifier, ) { val clickActionLabel = stringResource(R.string.core_ui_card_tap_action) + val sharingLabel = stringResource(R.string.core_ui_feed_sharing) + val sharingContent = stringResource( + R.string.core_ui_feed_sharing_data, + userNewsResource.title, + userNewsResource.url, + ) + + val dragAndDropFlags = if (VERSION.SDK_INT >= VERSION_CODES.N) { + View.DRAG_FLAG_GLOBAL + } else { + 0 + } + Card( onClick = onClick, shape = RoundedCornerShape(16.dp), @@ -112,7 +134,23 @@ fun NewsResourceCardExpanded( Row { NewsResourceTitle( userNewsResource.title, - modifier = Modifier.fillMaxWidth((.8f)), + modifier = Modifier + .fillMaxWidth((.8f)) + .dragAndDropSource { + detectTapGestures( + onLongPress = { + startTransfer( + DragAndDropTransferData( + ClipData.newPlainText( + sharingLabel, + sharingContent, + ), + flags = dragAndDropFlags, + ), + ) + }, + ) + }, ) Spacer(modifier = Modifier.weight(1f)) BookmarkButton(isBookmarked, onToggleBookmark) diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index ab76748ef..a97746a9c 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -29,4 +29,6 @@ Follow interest Unfollow interest + Feed sharing + %1$s: %2$s diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt index 13d0baef0..ea8d525ab 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt +++ b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt @@ -21,16 +21,18 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute +import kotlinx.serialization.Serializable -const val BOOKMARKS_ROUTE = "bookmarks_route" +@Serializable object BookmarksRoute -fun NavController.navigateToBookmarks(navOptions: NavOptions) = navigate(BOOKMARKS_ROUTE, navOptions) +fun NavController.navigateToBookmarks(navOptions: NavOptions) = + navigate(route = BookmarksRoute, navOptions) fun NavGraphBuilder.bookmarksScreen( onTopicClick: (String) -> Unit, onShowSnackbar: suspend (String, String?) -> Boolean, ) { - composable(route = BOOKMARKS_ROUTE) { + composable { BookmarksRoute(onTopicClick, onShowSnackbar) } } diff --git a/feature/foryou/build.gradle.kts b/feature/foryou/build.gradle.kts index 004fe8ad6..41d5b16a2 100644 --- a/feature/foryou/build.gradle.kts +++ b/feature/foryou/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { implementation(libs.accompanist.permissions) implementation(projects.core.data) implementation(projects.core.domain) + implementation(project(":core:notifications")) testImplementation(libs.hilt.android.testing) testImplementation(libs.robolectric) diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 885020636..0f345aa80 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -106,7 +106,7 @@ import com.google.samples.apps.nowinandroid.core.ui.launchCustomChromeTab import com.google.samples.apps.nowinandroid.core.ui.newsFeed @Composable -internal fun ForYouRoute( +internal fun ForYouScreen( onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, viewModel: ForYouViewModel = hiltViewModel(), diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index 85035a77a..4b6cd39c9 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -27,8 +27,8 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserDataReposit import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.util.SyncManager import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase +import com.google.samples.apps.nowinandroid.core.notifications.DEEP_LINK_NEWS_RESOURCE_ID_KEY import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.LINKED_NEWS_RESOURCE_ID import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -55,7 +55,7 @@ class ForYouViewModel @Inject constructor( userDataRepository.userData.map { !it.shouldHideOnboarding } val deepLinkedNewsResource = savedStateHandle.getStateFlow( - key = LINKED_NEWS_RESOURCE_ID, + key = DEEP_LINK_NEWS_RESOURCE_ID_KEY, null, ) .flatMapLatest { newsResourceId -> @@ -129,7 +129,7 @@ class ForYouViewModel @Inject constructor( fun onDeepLinkOpened(newsResourceId: String) { if (newsResourceId == deepLinkedNewsResource.value?.id) { - savedStateHandle[LINKED_NEWS_RESOURCE_ID] = null + savedStateHandle[DEEP_LINK_NEWS_RESOURCE_ID_KEY] = null } analyticsHelper.logNewsDeepLinkOpen(newsResourceId = newsResourceId) viewModelScope.launch { @@ -153,7 +153,7 @@ private fun AnalyticsHelper.logNewsDeepLinkOpen(newsResourceId: String) = type = "news_deep_link_opened", extras = listOf( Param( - key = LINKED_NEWS_RESOURCE_ID, + key = DEEP_LINK_NEWS_RESOURCE_ID_KEY, value = newsResourceId, ), ), diff --git a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt index 8e94a491a..9d98f1618 100644 --- a/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt +++ b/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt @@ -19,29 +19,31 @@ package com.google.samples.apps.nowinandroid.feature.foryou.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType import androidx.navigation.compose.composable -import androidx.navigation.navArgument import androidx.navigation.navDeepLink -import com.google.samples.apps.nowinandroid.feature.foryou.ForYouRoute +import com.google.samples.apps.nowinandroid.core.notifications.DEEP_LINK_URI_PATTERN +import com.google.samples.apps.nowinandroid.feature.foryou.ForYouScreen +import kotlinx.serialization.Serializable -const val LINKED_NEWS_RESOURCE_ID = "linkedNewsResourceId" -const val FOR_YOU_ROUTE = "for_you_route/{$LINKED_NEWS_RESOURCE_ID}" -private const val DEEP_LINK_URI_PATTERN = - "https://www.nowinandroid.apps.samples.google.com/foryou/{$LINKED_NEWS_RESOURCE_ID}" +@Serializable data object ForYouRoute -fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(FOR_YOU_ROUTE, navOptions) +fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(route = ForYouRoute, navOptions) fun NavGraphBuilder.forYouScreen(onTopicClick: (String) -> Unit) { - composable( - route = FOR_YOU_ROUTE, + composable( deepLinks = listOf( - navDeepLink { uriPattern = DEEP_LINK_URI_PATTERN }, - ), - arguments = listOf( - navArgument(LINKED_NEWS_RESOURCE_ID) { type = NavType.StringType }, + navDeepLink { + /** + * This destination has a deep link that enables a specific news resource to be + * opened from a notification (@see SystemTrayNotifier for more). The news resource + * ID is sent in the URI rather than being modelled in the route type because it's + * transient data (stored in SavedStateHandle) that is cleared after the user has + * opened the news resource. + */ + uriPattern = DEEP_LINK_URI_PATTERN + }, ), ) { - ForYouRoute(onTopicClick) + ForYouScreen(onTopicClick) } } diff --git a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt index 2fbdf0a79..eece140ac 100644 --- a/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature/foryou/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt @@ -26,6 +26,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.model.data.mapToUserNewsResources +import com.google.samples.apps.nowinandroid.core.notifications.DEEP_LINK_NEWS_RESOURCE_ID_KEY import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository @@ -34,7 +35,6 @@ import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.TestAnalyticsHelper import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncManager import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.LINKED_NEWS_RESOURCE_ID import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -472,7 +472,7 @@ class ForYouViewModelTest { newsRepository.sendNewsResources(sampleNewsResources) userDataRepository.setUserData(emptyUserData) - savedStateHandle[LINKED_NEWS_RESOURCE_ID] = sampleNewsResources.first().id + savedStateHandle[DEEP_LINK_NEWS_RESOURCE_ID_KEY] = sampleNewsResources.first().id assertEquals( expected = UserNewsResource( @@ -496,7 +496,7 @@ class ForYouViewModelTest { type = "news_deep_link_opened", extras = listOf( Param( - key = LINKED_NEWS_RESOURCE_ID, + key = DEEP_LINK_NEWS_RESOURCE_ID_KEY, value = sampleNewsResources.first().id, ), ), diff --git a/feature/interests/build.gradle.kts b/feature/interests/build.gradle.kts index ca91ba2c4..2b84b135f 100644 --- a/feature/interests/build.gradle.kts +++ b/feature/interests/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.domain) testImplementation(projects.core.testing) + testImplementation(libs.robolectric) androidTestImplementation(libs.bundles.androidx.compose.ui.test) androidTestImplementation(projects.core.testing) diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt index b369ac5ab..67cc8884f 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt +++ b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt @@ -19,11 +19,12 @@ package com.google.samples.apps.nowinandroid.feature.interests import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.domain.TopicSortField import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic -import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG +import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -39,7 +40,14 @@ class InterestsViewModel @Inject constructor( getFollowableTopics: GetFollowableTopicsUseCase, ) : ViewModel() { - val selectedTopicId: StateFlow = savedStateHandle.getStateFlow(TOPIC_ID_ARG, null) + // Key used to save and retrieve the currently selected topic id from saved state. + private val selectedTopicIdKey = "selectedTopicIdKey" + + private val interestsRoute: InterestsRoute = savedStateHandle.toRoute() + private val selectedTopicId = savedStateHandle.getStateFlow( + key = selectedTopicIdKey, + initialValue = interestsRoute.initialTopicId, + ) val uiState: StateFlow = combine( selectedTopicId, @@ -58,7 +66,7 @@ class InterestsViewModel @Inject constructor( } fun onTopicClick(topicId: String?) { - savedStateHandle[TOPIC_ID_ARG] = topicId + savedStateHandle[selectedTopicIdKey] = topicId } } diff --git a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt index 8a0f2d130..d83e4a9b2 100644 --- a/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt +++ b/feature/interests/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/navigation/InterestsNavigation.kt @@ -17,39 +17,17 @@ package com.google.samples.apps.nowinandroid.feature.interests.navigation import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute +import kotlinx.serialization.Serializable -const val TOPIC_ID_ARG = "topicId" -const val INTERESTS_ROUTE_BASE = "interests_route" -const val INTERESTS_ROUTE = "$INTERESTS_ROUTE_BASE?$TOPIC_ID_ARG={$TOPIC_ID_ARG}" +@Serializable data class InterestsRoute( + // The ID of the topic which will be initially selected at this destination + val initialTopicId: String? = null, +) -fun NavController.navigateToInterests(topicId: String? = null, navOptions: NavOptions? = null) { - val route = if (topicId != null) { - "${INTERESTS_ROUTE_BASE}?${TOPIC_ID_ARG}=$topicId" - } else { - INTERESTS_ROUTE_BASE - } - navigate(route, navOptions) -} - -fun NavGraphBuilder.interestsScreen( - onTopicClick: (String) -> Unit, +fun NavController.navigateToInterests( + initialTopicId: String? = null, + navOptions: NavOptions? = null, ) { - composable( - route = INTERESTS_ROUTE, - arguments = listOf( - navArgument(TOPIC_ID_ARG) { - defaultValue = null - nullable = true - type = NavType.StringType - }, - ), - ) { - InterestsRoute(onTopicClick = onTopicClick) - } + navigate(route = InterestsRoute(initialTopicId), navOptions) } diff --git a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt b/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt index 63d3c49b7..987a5bc01 100644 --- a/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt +++ b/feature/interests/src/test/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsViewModelTest.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.nowinandroid.interests import androidx.lifecycle.SavedStateHandle +import androidx.navigation.testing.invoke import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.Topic @@ -25,7 +26,7 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel -import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG +import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -33,12 +34,21 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner import kotlin.test.assertEquals /** * To learn more about how this test handles Flows created with stateIn, see * https://developer.android.com/kotlin/flow/test#statein + * + * These tests use Robolectric because the subject under test (the ViewModel) uses + * `SavedStateHandle.toRoute` which has a dependency on `android.os.Bundle`. + * + * TODO: Remove Robolectric if/when AndroidX Navigation API is updated to remove Android dependency. + * See https://issuetracker.google.com/340966212. */ +@RunWith(RobolectricTestRunner::class) class InterestsViewModelTest { @get:Rule @@ -55,7 +65,9 @@ class InterestsViewModelTest { @Before fun setup() { viewModel = InterestsViewModel( - savedStateHandle = SavedStateHandle(mapOf(TOPIC_ID_ARG to testInputTopics[0].topic.id)), + savedStateHandle = SavedStateHandle( + route = InterestsRoute(initialTopicId = testInputTopics[0].topic.id), + ), userDataRepository = userDataRepository, getFollowableTopics = getFollowableTopicsUseCase, ) diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts index c5f1f6ad0..5bb659c35 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/build.gradle.kts @@ -27,7 +27,6 @@ android { dependencies { implementation(projects.core.data) implementation(projects.core.domain) - implementation(projects.core.ui) testImplementation(projects.core.testing) diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt index 86b1eb717..ff91941a8 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt +++ b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchScreen.kt @@ -41,7 +41,6 @@ import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon @@ -66,6 +65,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString @@ -73,6 +73,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -227,23 +228,31 @@ fun EmptySearchResultBody( textAlign = TextAlign.Center, modifier = Modifier.padding(vertical = 24.dp), ) - val interests = stringResource(id = searchR.string.feature_search_interests) val tryAnotherSearchString = buildAnnotatedString { append(stringResource(id = searchR.string.feature_search_try_another_search)) append(" ") - withStyle( - style = SpanStyle( - textDecoration = TextDecoration.Underline, - fontWeight = FontWeight.Bold, + withLink( + LinkAnnotation.Clickable( + tag = "", + linkInteractionListener = { + onInterestsClick() + }, ), ) { - pushStringAnnotation(tag = interests, annotation = interests) - append(interests) + withStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + fontWeight = FontWeight.Bold, + ), + ) { + append(stringResource(id = searchR.string.feature_search_interests)) + } } + append(" ") append(stringResource(id = searchR.string.feature_search_to_browse_topics)) } - ClickableText( + Text( text = tryAnotherSearchString, style = MaterialTheme.typography.bodyLarge.merge( TextStyle( @@ -252,13 +261,8 @@ fun EmptySearchResultBody( ), ), modifier = Modifier - .padding(start = 36.dp, end = 36.dp, bottom = 24.dp) - .clickable {}, - ) { offset -> - tryAnotherSearchString.getStringAnnotations(start = offset, end = offset) - .firstOrNull() - ?.let { onInterestsClick() } - } + .padding(start = 36.dp, end = 36.dp, bottom = 24.dp), + ) } } diff --git a/feature/topic/build.gradle.kts b/feature/topic/build.gradle.kts index 726920af1..bd8b59ec8 100644 --- a/feature/topic/build.gradle.kts +++ b/feature/topic/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.data) testImplementation(projects.core.testing) + testImplementation(libs.robolectric) androidTestImplementation(libs.bundles.androidx.compose.ui.test) androidTestImplementation(projects.core.testing) diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index 5ac766675..13fbab784 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -71,7 +71,7 @@ import com.google.samples.apps.nowinandroid.core.ui.userNewsResourceCardItems import com.google.samples.apps.nowinandroid.feature.topic.R.string @Composable -internal fun TopicRoute( +internal fun TopicScreen( showBackButton: Boolean, onBackClick: () -> Unit, onTopicClick: (String) -> Unit, diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt index 255e40f8b..ba8baad14 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt @@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid.feature.topic import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository @@ -28,7 +29,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.asResult -import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicArgs +import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -47,12 +48,10 @@ class TopicViewModel @Inject constructor( userNewsResourceRepository: UserNewsResourceRepository, ) : ViewModel() { - private val topicArgs: TopicArgs = TopicArgs(savedStateHandle) - - val topicId = topicArgs.topicId + val topicId = savedStateHandle.toRoute().id val topicUiState: StateFlow = topicUiState( - topicId = topicArgs.topicId, + topicId = topicId, userDataRepository = userDataRepository, topicsRepository = topicsRepository, ) @@ -63,7 +62,7 @@ class TopicViewModel @Inject constructor( ) val newsUiState: StateFlow = newsUiState( - topicId = topicArgs.topicId, + topicId = topicId, userDataRepository = userDataRepository, userNewsResourceRepository = userNewsResourceRepository, ) @@ -75,7 +74,7 @@ class TopicViewModel @Inject constructor( fun followTopicToggle(followed: Boolean) { viewModelScope.launch { - userDataRepository.setTopicIdFollowed(topicArgs.topicId, followed) + userDataRepository.setTopicIdFollowed(topicId, followed) } } diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt index 394c53303..fabb82b10 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt +++ b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt @@ -16,53 +16,28 @@ package com.google.samples.apps.nowinandroid.feature.topic.navigation -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder -import androidx.navigation.NavType import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute -import java.net.URLDecoder -import java.net.URLEncoder -import kotlin.text.Charsets.UTF_8 +import com.google.samples.apps.nowinandroid.feature.topic.TopicScreen +import kotlinx.serialization.Serializable -private val URL_CHARACTER_ENCODING = UTF_8.name() - -@VisibleForTesting -internal const val TOPIC_ID_ARG = "topicId" -const val TOPIC_ROUTE = "topic_route" - -internal class TopicArgs(val topicId: String) { - constructor(savedStateHandle: SavedStateHandle) : - this(URLDecoder.decode(checkNotNull(savedStateHandle[TOPIC_ID_ARG]), URL_CHARACTER_ENCODING)) -} +@Serializable data class TopicRoute(val id: String) fun NavController.navigateToTopic(topicId: String, navOptions: NavOptionsBuilder.() -> Unit = {}) { - navigate(createTopicRoute(topicId)) { + navigate(route = TopicRoute(topicId)) { navOptions() } } -fun createTopicRoute(topicId: String): String { - val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING) - return "$TOPIC_ROUTE/$encodedId" -} - fun NavGraphBuilder.topicScreen( showBackButton: Boolean, onBackClick: () -> Unit, onTopicClick: (String) -> Unit, ) { - composable( - route = "topic_route/{$TOPIC_ID_ARG}", - arguments = listOf( - navArgument(TOPIC_ID_ARG) { type = NavType.StringType }, - ), - ) { - TopicRoute( + composable { + TopicScreen( showBackButton = showBackButton, onBackClick = onBackClick, onTopicClick = onTopicClick, diff --git a/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt b/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt index 565732f59..c14e62e31 100644 --- a/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt +++ b/feature/topic/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.nowinandroid.feature.topic import androidx.lifecycle.SavedStateHandle +import androidx.navigation.testing.invoke import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.NewsResource @@ -25,7 +26,7 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepo import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule -import com.google.samples.apps.nowinandroid.feature.topic.navigation.TOPIC_ID_ARG +import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first @@ -36,13 +37,22 @@ import kotlinx.datetime.Instant import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner import kotlin.test.assertEquals import kotlin.test.assertIs /** * To learn more about how this test handles Flows created with stateIn, see * https://developer.android.com/kotlin/flow/test#statein + * + * These tests use Robolectric because the subject under test (the ViewModel) uses + * `SavedStateHandle.toRoute` which has a dependency on `android.os.Bundle`. + * + * TODO: Remove Robolectric if/when AndroidX Navigation API is updated to remove Android dependency. + * * See b/340966212. */ +@RunWith(RobolectricTestRunner::class) class TopicViewModelTest { @get:Rule @@ -60,7 +70,9 @@ class TopicViewModelTest { @Before fun setup() { viewModel = TopicViewModel( - savedStateHandle = SavedStateHandle(mapOf(TOPIC_ID_ARG to testInputTopics[0].topic.id)), + savedStateHandle = SavedStateHandle( + route = TopicRoute(id = testInputTopics[0].topic.id), + ), userDataRepository = userDataRepository, topicsRepository = topicsRepository, userNewsResourceRepository = userNewsResourceRepository, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7047ac665..ba9c36bfd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,15 +2,12 @@ accompanist = "0.34.0" androidDesugarJdkLibs = "2.0.4" # AGP and tools should be updated together -androidGradlePlugin = "8.4.0" -androidTools = "31.4.1" +androidGradlePlugin = "8.6.0" +androidTools = "31.6.0" androidxActivity = "1.8.2" androidxAppCompat = "1.7.0" androidxBrowser = "1.8.0" -androidxComposeAlpha = "1.7.0-beta01" -androidxComposeBom = "2024.02.02" -androidxComposeMaterial3Adaptive = "1.0.0-beta01" -androidxComposeMaterial3AdaptiveNavigationSuite = "1.3.0-beta01" +androidxComposeBom = "2024.09.00" androidxComposeRuntimeTracing = "1.0.0-beta01" androidxCore = "1.12.0" androidxCoreSplashscreen = "1.0.1" @@ -20,7 +17,7 @@ androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.8.3" androidxMacroBenchmark = "1.2.4" androidxMetrics = "1.0.0-alpha04" -androidxNavigation = "2.8.0-alpha06" +androidxNavigation = "2.8.0" androidxProfileinstaller = "1.3.1" androidxTestCore = "1.5.0" androidxTestExt = "1.1.5" @@ -28,7 +25,7 @@ androidxTestRules = "1.5.0" androidxTestRunner = "1.5.2" androidxTracing = "1.3.0-alpha02" androidxUiAutomator = "2.3.0" -androidxWindowManager = "1.3.0-alpha03" +androidxWindowManager = "1.3.0" androidxWork = "2.9.0" coil = "2.6.0" dependencyGuard = "0.5.0" @@ -42,11 +39,11 @@ hilt = "2.51.1" hiltExt = "1.1.0" jacoco = "0.8.7" junit4 = "4.13.2" -kotlin = "2.0.0" +kotlin = "2.0.20" kotlinxCoroutines = "1.8.0" kotlinxDatetime = "0.5.0" kotlinxSerializationJson = "1.6.3" -ksp = "2.0.0-1.0.21" +ksp = "2.0.20-1.0.24" moduleGraph = "2.5.0" okhttp = "4.12.0" protobuf = "4.26.1" @@ -71,18 +68,18 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version androidx-benchmark-macro = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "androidxMacroBenchmark" } androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } -androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidxComposeAlpha" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } -androidx-compose-material3-navigationSuite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite", version.ref = "androidxComposeMaterial3AdaptiveNavigationSuite" } -androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "androidxComposeMaterial3Adaptive" } -androidx-compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "androidxComposeMaterial3Adaptive" } -androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "androidxComposeMaterial3Adaptive" } +androidx-compose-material3-navigationSuite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" } +androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive" } +androidx-compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout" } +androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation" } androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class" } androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidxComposeRuntimeTracing" } -androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "androidxComposeAlpha" } +androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } @@ -128,6 +125,7 @@ hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.r hilt-ext-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltExt" } javax-inject = { module = "javax.inject:javax.inject", version = "1" } kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "kotlinxCoroutines" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f..a4b76b953 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4..9355b4155 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426..f5feea6d6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 7101f8e46..9b42019c7 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ##########################################################################