Merge branch 'main' into introduce_screenshot_a11y_tests

pull/1708/head
Don Turner 2 weeks ago committed by GitHub
commit a258794dc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,3 +5,13 @@
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
ktlint_function_naming_ignore_when_annotated_with=Composable, Test
ktlint_standard_backing-property-naming = disabled
ktlint_standard_binary-expression-wrapping = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_condition-wrapping = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-literal = disabled
ktlint_standard_function-type-modifier-spacing = disabled
ktlint_standard_multiline-loop = disabled
ktlint_standard_function-signature = disabled

@ -93,7 +93,9 @@ jobs:
continue-on-error: false
if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "::error::Screenshot tests failed, please create a PR in your fork first." && exit 1
echo "::error::Screenshot tests failed, please create a PR in your fork first."
echo "Your fork's CI will take screenshots for your fork."
exit 1
# Runs if previous job failed
- name: Generate new screenshots if verification failed and it's a PR
@ -166,7 +168,7 @@ jobs:
timeout-minutes: 55
strategy:
matrix:
api-level: [26, 30]
api-level: [26, 34]
steps:
- name: Delete unnecessary tools 🔧
@ -235,7 +237,7 @@ jobs:
- name: Display local test coverage (only API 30)
if: matrix.api-level == 30
id: jacoco
uses: madrapps/jacoco-report@v1.7.0
uses: madrapps/jacoco-report@v1.7.1
with:
title: Combined test coverage report
min-coverage-overall: 40

@ -123,7 +123,6 @@ dependencies {
testDemoImplementation(libs.roborazzi)
testDemoImplementation(projects.core.screenshotTesting)
androidTestImplementation(kotlin("test"))
androidTestImplementation(projects.core.testing)
androidTestImplementation(projects.core.dataTest)
androidTestImplementation(projects.core.datastoreTest)
@ -131,6 +130,7 @@ dependencies {
androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.androidx.compose.ui.test)
androidTestImplementation(libs.hilt.android.testing)
androidTestImplementation(libs.kotlin.test)
baselineProfile(projects.benchmarks)
}

@ -89,38 +89,38 @@ androidx.hilt:hilt-navigation:1.2.0
androidx.hilt:hilt-work:1.2.0
androidx.interpolator:interpolator:1.0.0
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.8.6
androidx.lifecycle:lifecycle-common-jvm:2.8.6
androidx.lifecycle:lifecycle-common:2.8.6
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.6
androidx.lifecycle:lifecycle-livedata-core:2.8.6
androidx.lifecycle:lifecycle-livedata:2.8.6
androidx.lifecycle:lifecycle-process:2.8.6
androidx.lifecycle:lifecycle-runtime-android:2.8.6
androidx.lifecycle:lifecycle-runtime-compose-android:2.8.6
androidx.lifecycle:lifecycle-runtime-compose:2.8.6
androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.6
androidx.lifecycle:lifecycle-runtime-ktx:2.8.6
androidx.lifecycle:lifecycle-runtime:2.8.6
androidx.lifecycle:lifecycle-service:2.8.6
androidx.lifecycle:lifecycle-viewmodel-android:2.8.6
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.6
androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6
androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.6
androidx.lifecycle:lifecycle-viewmodel:2.8.6
androidx.lifecycle:lifecycle-common-java8:2.8.7
androidx.lifecycle:lifecycle-common-jvm:2.8.7
androidx.lifecycle:lifecycle-common:2.8.7
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.7
androidx.lifecycle:lifecycle-livedata-core:2.8.7
androidx.lifecycle:lifecycle-livedata:2.8.7
androidx.lifecycle:lifecycle-process:2.8.7
androidx.lifecycle:lifecycle-runtime-android:2.8.7
androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7
androidx.lifecycle:lifecycle-runtime-compose:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx:2.8.7
androidx.lifecycle:lifecycle-runtime:2.8.7
androidx.lifecycle:lifecycle-service:2.8.7
androidx.lifecycle:lifecycle-viewmodel-android:2.8.7
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.7
androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7
androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7
androidx.lifecycle:lifecycle-viewmodel:2.8.7
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.metrics:metrics-performance:1.0.0-beta01
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.navigation:navigation-common-ktx:2.8.4
androidx.navigation:navigation-common:2.8.4
androidx.navigation:navigation-compose:2.8.4
androidx.navigation:navigation-runtime-ktx:2.8.4
androidx.navigation:navigation-runtime:2.8.4
androidx.print:print:1.0.0
androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05
androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05
androidx.profileinstaller:profileinstaller:1.3.1
androidx.profileinstaller:profileinstaller:1.4.1
androidx.resourceinspection:resourceinspection-annotation:1.0.1
androidx.room:room-common:2.6.1
androidx.room:room-ktx:2.6.1
@ -141,11 +141,11 @@ androidx.window.extensions.core:core:1.0.0
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
androidx.work:work-runtime-ktx:2.9.1
androidx.work:work-runtime:2.9.1
com.caverock:androidsvg-aar:1.4
com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.accompanist:accompanist-permissions:0.34.0
com.google.accompanist:accompanist-permissions:0.36.0
com.google.android.datatransport:transport-api:3.2.0
com.google.android.datatransport:transport-backend-cct:3.3.0
com.google.android.datatransport:transport-runtime:3.3.0
@ -153,12 +153,12 @@ com.google.android.gms:play-services-ads-identifier:18.0.0
com.google.android.gms:play-services-base:18.5.0
com.google.android.gms:play-services-basement:18.4.0
com.google.android.gms:play-services-cloud-messaging:17.2.0
com.google.android.gms:play-services-measurement-api:22.1.0
com.google.android.gms:play-services-measurement-base:22.1.0
com.google.android.gms:play-services-measurement-impl:22.1.0
com.google.android.gms:play-services-measurement-sdk-api:22.1.0
com.google.android.gms:play-services-measurement-sdk:22.1.0
com.google.android.gms:play-services-measurement:22.1.0
com.google.android.gms:play-services-measurement-api:22.1.2
com.google.android.gms:play-services-measurement-base:22.1.2
com.google.android.gms:play-services-measurement-impl:22.1.2
com.google.android.gms:play-services-measurement-sdk-api:22.1.2
com.google.android.gms:play-services-measurement-sdk:22.1.2
com.google.android.gms:play-services-measurement:22.1.2
com.google.android.gms:play-services-oss-licenses:17.1.0
com.google.android.gms:play-services-stats:17.0.2
com.google.android.gms:play-services-tasks:18.2.0
@ -169,15 +169,15 @@ com.google.dagger:hilt-android:2.52
com.google.dagger:hilt-core:2.52
com.google.errorprone:error_prone_annotations:2.26.0
com.google.firebase:firebase-abt:21.1.1
com.google.firebase:firebase-analytics:22.1.0
com.google.firebase:firebase-analytics:22.1.2
com.google.firebase:firebase-annotations:16.2.0
com.google.firebase:firebase-bom:33.3.0
com.google.firebase:firebase-bom:33.7.0
com.google.firebase:firebase-common-ktx:21.0.0
com.google.firebase:firebase-common:21.0.0
com.google.firebase:firebase-components:18.0.0
com.google.firebase:firebase-config-interop:16.0.1
com.google.firebase:firebase-config:22.0.0
com.google.firebase:firebase-crashlytics:19.1.0
com.google.firebase:firebase-config:22.0.1
com.google.firebase:firebase-crashlytics:19.3.0
com.google.firebase:firebase-datatransport:19.0.0
com.google.firebase:firebase-encoders-json:18.0.1
com.google.firebase:firebase-encoders-proto:16.0.0
@ -186,16 +186,15 @@ com.google.firebase:firebase-iid-interop:17.1.0
com.google.firebase:firebase-installations-interop:17.2.0
com.google.firebase:firebase-installations:18.0.0
com.google.firebase:firebase-measurement-connector:20.0.1
com.google.firebase:firebase-messaging:24.0.1
com.google.firebase:firebase-perf:21.0.1
com.google.firebase:firebase-sessions:2.0.4
com.google.firebase:protolite-well-known-types:18.0.0
com.google.firebase:firebase-messaging:24.1.0
com.google.firebase:firebase-perf:21.0.3
com.google.firebase:firebase-sessions:2.0.7
com.google.guava:failureaccess:1.0.1
com.google.guava:guava:31.1-android
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
com.google.j2objc:j2objc-annotations:1.3
com.google.protobuf:protobuf-javalite:4.26.1
com.google.protobuf:protobuf-kotlin-lite:4.26.1
com.google.protobuf:protobuf-javalite:4.28.2
com.google.protobuf:protobuf-kotlin-lite:4.28.2
com.squareup.okhttp3:logging-interceptor:4.12.0
com.squareup.okhttp3:okhttp:4.12.0
com.squareup.okio:okio-jvm:3.9.0

@ -37,7 +37,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult.ActionPerformed
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
@ -76,7 +75,6 @@ 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)
@Composable
fun NiaApp(
appState: NiaAppState,
@ -126,7 +124,6 @@ fun NiaApp(
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalComposeUiApi::class,
ExperimentalMaterial3AdaptiveApi::class,
)
internal fun NiaApp(
appState: NiaAppState,

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

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

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

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

@ -21,6 +21,7 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.exclude
class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
@ -35,7 +36,16 @@ class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
val bom = libs.findLibrary("firebase-bom").get()
add("implementation", platform(bom))
"implementation"(libs.findLibrary("firebase.analytics").get())
"implementation"(libs.findLibrary("firebase.performance").get())
"implementation"(libs.findLibrary("firebase.performance").get()) {
/*
Exclusion of protobuf / protolite dependencies is necessary as the
datastore-proto brings in protobuf dependencies. These are the source of truth
for Now in Android.
That's why the duplicate classes from below dependencies are excluded.
*/
exclude(group = "com.google.protobuf", module = "protobuf-javalite")
exclude(group = "com.google.firebase", module = "protolite-well-known-types")
}
"implementation"(libs.findLibrary("firebase.crashlytics").get())
}

@ -53,8 +53,8 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
disableUnnecessaryAndroidTests(target)
}
dependencies {
add("androidTestImplementation", kotlin("test"))
add("testImplementation", kotlin("test"))
add("androidTestImplementation", libs.findLibrary("kotlin.test").get())
add("testImplementation", libs.findLibrary("kotlin.test").get())
add("implementation", libs.findLibrary("androidx.tracing.ktx").get())
}

@ -15,6 +15,7 @@
*/
import com.google.samples.apps.nowinandroid.configureKotlinJvm
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
@ -29,7 +30,7 @@ class JvmLibraryConventionPlugin : Plugin<Project> {
}
configureKotlinJvm()
dependencies {
add("testImplementation", kotlin("test"))
add("testImplementation", libs.findLibrary("kotlin.test").get())
}
}
}

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

@ -90,5 +90,19 @@ private inline fun <reified T : KotlinTopLevelExtension> Project.configureKotlin
// Enable experimental coroutines APIs, including Flow
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
)
freeCompilerArgs.add(
/**
* Remove this args after Phase 3.
* https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-consistent-copy-visibility/#deprecation-timeline
*
* Deprecation timeline
* Phase 3. (Supposedly Kotlin 2.2 or Kotlin 2.3).
* The default changes.
* Unless ExposedCopyVisibility is used, the generated 'copy' method has the same visibility as the primary constructor.
* The binary signature changes. The error on the declaration is no longer reported.
* '-Xconsistent-data-class-copy-visibility' compiler flag and ConsistentCopyVisibility annotation are now unnecessary.
*/
"-Xconsistent-data-class-copy-visibility"
)
}
}

@ -16,7 +16,13 @@
dependencyResolutionManagement {
repositories {
google()
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
}
versionCatalogs {

@ -16,7 +16,13 @@
buildscript {
repositories {
google()
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
// Android Build Server

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

@ -22,7 +22,6 @@ 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,

@ -21,7 +21,6 @@ import android.net.Uri
import androidx.annotation.ColorInt
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
@ -45,7 +44,6 @@ import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
* An extension on [LazyListScope] defining a feed with news resources.
* Depending on the [feedState], this might emit no items.
*/
@OptIn(ExperimentalFoundationApi::class)
fun LazyStaggeredGridScope.newsFeed(
feedState: NewsFeedUiState,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 302 KiB

@ -18,7 +18,6 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks
import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.filter
@ -33,6 +32,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.testing.TestLifecycleOwner
import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState

@ -29,7 +29,7 @@ dependencies {
implementation(libs.accompanist.permissions)
implementation(projects.core.data)
implementation(projects.core.domain)
implementation(project(":core:notifications"))
implementation(projects.core.notifications)
testImplementation(libs.hilt.android.testing)
testImplementation(libs.robolectric)

@ -522,6 +522,7 @@ private fun SearchTextField(
.focusRequester(focusRequester)
.onKeyEvent {
if (it.key == Key.Enter) {
if (searchQuery.isBlank()) return@onKeyEvent false
onSearchExplicitlyTriggered()
true
} else {
@ -536,6 +537,7 @@ private fun SearchTextField(
),
keyboardActions = KeyboardActions(
onSearch = {
if (searchQuery.isBlank()) return@KeyboardActions
onSearchExplicitlyTriggered()
},
),

@ -59,7 +59,7 @@ class SearchViewModel @Inject constructor(
flowOf(SearchResultUiState.SearchNotReady)
} else {
searchQuery.flatMapLatest { query ->
if (query.length < SEARCH_QUERY_MIN_LENGTH) {
if (query.trim().length < SEARCH_QUERY_MIN_LENGTH) {
flowOf(SearchResultUiState.EmptyQuery)
} else {
getSearchContentsUseCase(query)
@ -102,6 +102,7 @@ class SearchViewModel @Inject constructor(
* search query in the search text field, defining this method.
*/
fun onSearchTriggered(query: String) {
if (query.isBlank()) return
viewModelScope.launch {
recentSearchRepository.insertOrReplaceRecentSearch(searchQuery = query)
}

@ -21,10 +21,12 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.google.samples.apps.nowinandroid.feature.search.SearchRoute
import kotlinx.serialization.Serializable
const val SEARCH_ROUTE = "search_route"
@Serializable data object SearchRoute
fun NavController.navigateToSearch(navOptions: NavOptions? = null) = navigate(SEARCH_ROUTE, navOptions)
fun NavController.navigateToSearch(navOptions: NavOptions? = null) =
navigate(SearchRoute, navOptions)
fun NavGraphBuilder.searchScreen(
onBackClick: () -> Unit,
@ -33,7 +35,7 @@ fun NavGraphBuilder.searchScreen(
) {
// TODO: Handle back stack for each top-level destination. At the moment each top-level
// destination may have own search screen's back stack.
composable(route = SEARCH_ROUTE) {
composable<SearchRoute> {
SearchRoute(
onBackClick = onBackClick,
onInterestsClick = onInterestsClick,

@ -41,6 +41,7 @@ import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNull
/**
* To learn more about how this test handles Flows created with stateIn, see
@ -122,6 +123,43 @@ class SearchViewModelTest {
assertEquals(SearchNotReady, viewModel.searchResultUiState.value)
}
@Test
fun emptySearchText_isNotAddedToRecentSearches() = runTest {
viewModel.onSearchTriggered("")
val recentSearchQueriesStream = getRecentQueryUseCase()
val recentSearchQueries = recentSearchQueriesStream.first()
val recentSearchQuery = recentSearchQueries.firstOrNull()
assertNull(recentSearchQuery)
}
@Test
fun searchTextWithThreeSpaces_isEmptyQuery() = runTest {
searchContentsRepository.addNewsResources(newsResourcesTestData)
searchContentsRepository.addTopics(topicsTestData)
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() }
viewModel.onSearchQueryChanged(" ")
assertIs<EmptyQuery>(viewModel.searchResultUiState.value)
collectJob.cancel()
}
@Test
fun searchTextWithThreeSpacesAndOneLetter_isEmptyQuery() = runTest {
searchContentsRepository.addNewsResources(newsResourcesTestData)
searchContentsRepository.addTopics(topicsTestData)
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() }
viewModel.onSearchQueryChanged(" a")
assertIs<EmptyQuery>(viewModel.searchResultUiState.value)
collectJob.cancel()
}
@Test
fun whenToggleNewsResourceSavedIsCalled_bookmarkStateIsUpdated() = runTest {
val newsResourceId = "123"

@ -27,7 +27,8 @@ then
echo "The 'dot' command is not found. This is required to generate SVGs from the Graphviz files."
echo "Installation instructions:"
echo " - On macOS: You can install Graphviz using Homebrew with the command: 'brew install graphviz'"
echo " - On Ubuntu: You can install Graphviz using APT with the command: 'sudo apt-get install graphviz'"
echo " - On Ubuntu: You can install Graphviz using APT with the command: 'sudo apt install graphviz'"
echo " - Others: Visit https://graphviz.org/download/"
exit 1
fi

@ -14,10 +14,10 @@
* limitations under the License.
*/
val ktlintVersion = "1.0.1"
val ktlintVersion = "1.4.0"
initscript {
val spotlessVersion = "6.23.3"
val spotlessVersion = "6.25.0"
repositories {
mavenCentral()

@ -1,9 +1,9 @@
[versions]
accompanist = "0.34.0"
androidDesugarJdkLibs = "2.0.4"
accompanist = "0.36.0"
androidDesugarJdkLibs = "2.1.3"
# AGP and tools should be updated together
androidGradlePlugin = "8.6.1"
androidTools = "31.7.2"
androidTools = "31.7.3"
androidxActivity = "1.9.3"
androidxAppCompat = "1.7.0"
androidxBrowser = "1.8.0"
@ -14,11 +14,11 @@ androidxCoreSplashscreen = "1.0.1"
androidxDataStore = "1.1.1"
androidxEspresso = "3.6.1"
androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.8.6"
androidxMacroBenchmark = "1.3.0"
androidxLifecycle = "2.8.7"
androidxMacroBenchmark = "1.3.3"
androidxMetrics = "1.0.0-beta01"
androidxNavigation = "2.8.0"
androidxProfileinstaller = "1.3.1"
androidxNavigation = "2.8.4"
androidxProfileinstaller = "1.4.1"
androidxTestCore = "1.6.1"
androidxTestExt = "1.2.1"
androidxTestRules = "1.6.1"
@ -26,13 +26,13 @@ androidxTestRunner = "1.6.2"
androidxTracing = "1.3.0-alpha02"
androidxUiAutomator = "2.3.0"
androidxWindowManager = "1.3.0"
androidxWork = "2.9.0"
androidxWork = "2.9.1"
coil = "2.7.0"
dependencyGuard = "0.5.0"
firebaseBom = "33.3.0"
firebaseCrashlyticsPlugin = "2.9.9"
firebaseBom = "33.7.0"
firebaseCrashlyticsPlugin = "3.0.2"
firebasePerfPlugin = "1.4.2"
gmsPlugin = "4.4.1"
gmsPlugin = "4.4.2"
googleOss = "17.1.0"
googleOssPlugin = "0.10.6"
hilt = "2.52"
@ -46,15 +46,15 @@ kotlinxSerializationJson = "1.6.3"
ksp = "2.0.20-1.0.25"
moduleGraph = "2.7.1"
okhttp = "4.12.0"
protobuf = "4.26.1"
protobuf = "4.28.2"
protobufPlugin = "0.9.4"
retrofit = "2.11.0"
retrofitKotlinxSerializationJson = "1.0.0"
robolectric = "4.14.1"
roborazzi = "1.33.0"
roborazzi = "1.36.0"
room = "2.6.1"
secrets = "2.0.1"
truth = "1.4.2"
truth = "1.4.4"
turbine = "1.1.0"
[bundles]

@ -38,7 +38,7 @@ kotlin {
dependencies {
compileOnly(libs.kotlin.stdlib)
compileOnly(libs.lint.api)
testImplementation(libs.kotlin.test)
testImplementation(libs.lint.checks)
testImplementation(libs.lint.tests)
testImplementation(kotlin("test"))
}

@ -17,7 +17,13 @@
pluginManagement {
includeBuild("build-logic")
repositories {
google()
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
@ -26,7 +32,13 @@ pluginManagement {
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
google()
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
}
}

Loading…
Cancel
Save