diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 2e6d11841..5f501b6c0 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -45,13 +45,40 @@ jobs: - name: Check spotless run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache + - name: Check Dependency Guard + id: dependencyguard_verify + continue-on-error: true + run: ./gradlew dependencyGuard + + - name: Prevent updating Dependency Guard baselines if this is a fork + id: checkfork_dependencyguard + continue-on-error: false + if: steps.dependencyguard_verify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository + run: | + echo "::error::Dependency Guard failed, please update baselines with: ./gradlew dependencyGuardBaseline" && exit 1 + + # Runs if previous job failed + - name: Generate new Dependency Guard baselines if verification failed and it's a PR + id: dependencyguard_baseline + if: steps.dependencyguard_verify.outcome == 'failure' && github.event_name == 'pull_request' + run: | + ./gradlew dependencyGuardBaseline + + - name: Push new Dependency Guard baselines if available + uses: stefanzweifel/git-auto-commit-action@v5 + if: steps.dependencyguard_baseline.outcome == 'success' + with: + file_pattern: '**/dependencies/*.txt' + disable_globbing: true + commit_message: "🤖 Updates baselines for Dependency Guard" + - name: Run all local screenshot tests (Roborazzi) id: screenshotsverify continue-on-error: true run: ./gradlew verifyRoborazziDemoDebug - name: Prevent pushing new screenshots if this is a fork - id: checkfork + id: checkfork_screenshots continue-on-error: false if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository run: | @@ -82,13 +109,9 @@ jobs: - name: Build all build type and flavor permutations run: ./gradlew :app:assemble :benchmarks:assemble -x pixel6Api33ProdNonMinifiedReleaseAndroidTest - -x pixel6Api33ProdNonMinifiedBenchmarkAndroidTest -x pixel6Api33DemoNonMinifiedReleaseAndroidTest - -x pixel6Api33DemoNonMinifiedBenchmarkAndroidTest -x collectDemoNonMinifiedReleaseBaselineProfile - -x collectDemoNonMinifiedBenchmarkBaselineProfile -x collectProdNonMinifiedReleaseBaselineProfile - -x collectProdNonMinifiedBenchmarkBaselineProfile - name: Upload build outputs (APKs) uses: actions/upload-artifact@v4 @@ -117,13 +140,31 @@ jobs: run: ./gradlew :app:checkProdReleaseBadging androidTest: - runs-on: macOS-latest # enables hardware acceleration in the virtual machine + runs-on: ubuntu-latest timeout-minutes: 55 strategy: matrix: api-level: [26, 30] steps: + - name: Delete unnecessary tools 🔧 + uses: jlumbroso/free-disk-space@v1.3.1 + with: + android: false # Don't remove Android tools + tool-cache: true # Remove image tool cache - rm -rf "$AGENT_TOOLSDIRECTORY" + dotnet: true # rm -rf /usr/share/dotnet + haskell: true # rm -rf /opt/ghc... + swap-storage: true # rm -f /mnt/swapfile (4GiB) + docker-images: false # Takes 16s, enable if needed in the future + large-packages: false # includes google-cloud-sdk and it's slow + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + ls /dev/kvm + - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index f4901b9e2..f738ae105 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -7,10 +7,17 @@ on: jobs: build: - runs-on: macos-latest + runs-on: ubuntu-latest timeout-minutes: 120 steps: + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + ls /dev/kvm + - name: Checkout uses: actions/checkout@v4 diff --git a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt index 10f2dfa32..c3f83d734 100644 --- a/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt +++ b/app-nia-catalog/dependencies/releaseRuntimeClasspath.txt @@ -70,7 +70,8 @@ androidx.profileinstaller:profileinstaller:1.3.1 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 androidx.startup:startup-runtime:1.1.1 -androidx.tracing:tracing:1.0.0 +androidx.tracing:tracing-ktx:1.3.0-alpha02 +androidx.tracing:tracing:1.3.0-alpha02 androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 38166f7ff..520baa134 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,17 +57,6 @@ android { // Ensure Baseline Profile is fresh for release builds. baselineProfile.automaticGenerationDuringBuild = true } - create("benchmark") { - // Enable all the optimizations from release build through initWith(release). - initWith(release) - matchingFallbacks.add("release") - // Debug key signing is available to everyone. - signingConfig = signingConfigs.getByName("debug") - // Only use benchmark proguard rules - proguardFiles("benchmark-rules.pro") - isMinifyEnabled = true - applicationIdSuffix = NiaBuildType.BENCHMARK.applicationIdSuffix - } } packaging { diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt index 3f88c60ec..eaeff771a 100644 --- a/app/dependencies/prodReleaseRuntimeClasspath.txt +++ b/app/dependencies/prodReleaseRuntimeClasspath.txt @@ -71,19 +71,20 @@ androidx.hilt:hilt-navigation:1.0.0 androidx.hilt:hilt-work:1.1.0 androidx.interpolator:interpolator:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.6.2 -androidx.lifecycle:lifecycle-common:2.6.2 -androidx.lifecycle:lifecycle-livedata-core:2.6.2 -androidx.lifecycle:lifecycle-livedata:2.6.2 -androidx.lifecycle:lifecycle-process:2.6.2 -androidx.lifecycle:lifecycle-runtime-compose:2.6.2 -androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 -androidx.lifecycle:lifecycle-runtime:2.6.2 -androidx.lifecycle:lifecycle-service:2.6.2 -androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2 -androidx.lifecycle:lifecycle-viewmodel:2.6.2 +androidx.lifecycle:lifecycle-common-java8:2.7.0 +androidx.lifecycle:lifecycle-common:2.7.0 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0 +androidx.lifecycle:lifecycle-livedata-core:2.7.0 +androidx.lifecycle:lifecycle-livedata:2.7.0 +androidx.lifecycle:lifecycle-process:2.7.0 +androidx.lifecycle:lifecycle-runtime-compose:2.7.0 +androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 +androidx.lifecycle:lifecycle-runtime:2.7.0 +androidx.lifecycle:lifecycle-service:2.7.0 +androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 +androidx.lifecycle:lifecycle-viewmodel:2.7.0 androidx.loader:loader:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.metrics:metrics-performance:1.0.0-alpha04 @@ -105,15 +106,15 @@ androidx.savedstate:savedstate:1.2.1 androidx.sqlite:sqlite-framework:2.4.0 androidx.sqlite:sqlite:2.4.0 androidx.startup:startup-runtime:1.1.1 -androidx.tracing:tracing-ktx:1.1.0 -androidx.tracing:tracing:1.1.0 +androidx.tracing:tracing-ktx:1.3.0-alpha02 +androidx.tracing:tracing:1.3.0-alpha02 androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 androidx.window:window:1.0.0 -androidx.work:work-runtime-ktx:2.9.0-rc01 -androidx.work:work-runtime:2.9.0-rc01 +androidx.work:work-runtime-ktx:2.9.0 +androidx.work:work-runtime:2.9.0 com.caverock:androidsvg-aar:1.4 com.google.accompanist:accompanist-drawablepainter:0.32.0 com.google.accompanist:accompanist-permissions:0.32.0 @@ -168,8 +169,8 @@ com.google.guava:failureaccess:1.0.1 com.google.guava:guava:31.1-android com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava com.google.j2objc:j2objc-annotations:1.3 -com.google.protobuf:protobuf-javalite:3.24.4 -com.google.protobuf:protobuf-kotlin-lite:3.24.4 +com.google.protobuf:protobuf-javalite:3.25.2 +com.google.protobuf:protobuf-kotlin-lite:3.25.2 com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0 com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt index 7a7a82180..13f11c3b9 100644 --- a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -90,7 +90,7 @@ class NavigationTest { lateinit var topicsRepository: TopicsRepository private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = - ReadOnlyProperty { _, _ -> activity.getString(resId) } + ReadOnlyProperty { _, _ -> activity.getString(resId) } // The strings used for matching in these tests private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_navigate_up) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt new file mode 100644 index 000000000..595166f03 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.util + +import android.util.Log +import androidx.profileinstaller.ProfileVerifier +import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.guava.await +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * Logs the app's Baseline Profile Compilation Status using [ProfileVerifier]. + * + * When delivering through Google Play, the baseline profile is compiled during installation. + * In this case you will see the correct state logged without any further action necessary. + * To verify baseline profile installation locally, you need to manually trigger baseline + * profile installation. + * + * For immediate compilation, call: + * ```bash + * adb shell cmd package compile -f -m speed-profile com.example.macrobenchmark.target + * ``` + * You can also trigger background optimizations: + * ```bash + * adb shell pm bg-dexopt-job + * ``` + * Both jobs run asynchronously and might take some time complete. + * + * To see quick turnaround of the ProfileVerifier, we recommend using `speed-profile`. + * If you don't do either of these steps, you might only see the profile status reported as + * "enqueued for compilation" when running the sample locally. + * + * @see androidx.profileinstaller.ProfileVerifier.CompilationStatus.ResultCode + */ +class ProfileVerifierLogger @Inject constructor( + @ApplicationScope private val scope: CoroutineScope, +) { + companion object { + private const val TAG = "ProfileInstaller" + } + + operator fun invoke() = scope.launch { + val status = ProfileVerifier.getCompilationStatusAsync().await() + Log.d(TAG, "Status code: ${status.profileInstallResultCode}") + Log.d( + TAG, + when { + status.isCompiledWithProfile -> "App compiled with profile" + status.hasProfileEnqueuedForCompilation() -> "Profile enqueued for compilation" + else -> "Profile not compiled nor enqueued" + }, + ) + } +} 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 1f2bc49b9..6ce134ef4 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 @@ -17,7 +17,6 @@ package com.google.samples.apps.nowinandroid import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent @@ -37,7 +36,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.metrics.performance.JankStats -import androidx.profileinstaller.ProfileVerifier import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading import com.google.samples.apps.nowinandroid.MainActivityUiState.Success import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper @@ -49,12 +47,9 @@ import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand import com.google.samples.apps.nowinandroid.ui.NiaApp import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.guava.await import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject private const val TAG = "MainActivity" @@ -90,9 +85,7 @@ class MainActivity : ComponentActivity() { lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState - .onEach { - uiState = it - } + .onEach { uiState = it } .collect() } } @@ -152,48 +145,12 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() lazyStats.get().isTrackingEnabled = true - lifecycleScope.launch { - logCompilationStatus() - } } override fun onPause() { super.onPause() lazyStats.get().isTrackingEnabled = false } - - /** - * Logs the app's Baseline Profile Compilation Status using [ProfileVerifier]. - */ - private suspend fun logCompilationStatus() { - /* - When delivering through Google Play, the baseline profile is compiled during installation. - In this case you will see the correct state logged without any further action necessary. - To verify baseline profile installation locally, you need to manually trigger baseline - profile installation. - For immediate compilation, call: - `adb shell cmd package compile -f -m speed-profile com.example.macrobenchmark.target` - You can also trigger background optimizations: - `adb shell pm bg-dexopt-job` - Both jobs run asynchronously and might take some time complete. - To see quick turnaround of the ProfileVerifier, we recommend using `speed-profile`. - If you don't do either of these steps, you might only see the profile status reported as - "enqueued for compilation" when running the sample locally. - */ - withContext(Dispatchers.IO) { - val status = ProfileVerifier.getCompilationStatusAsync().await() - Log.d(TAG, "ProfileInstaller status code: ${status.profileInstallResultCode}") - Log.d( - TAG, - when { - status.isCompiledWithProfile -> "ProfileInstaller: is compiled with profile" - status.hasProfileEnqueuedForCompilation() -> - "ProfileInstaller: Enqueued for compilation" - else -> "Profile not compiled or enqueued" - }, - ) - } - } } /** diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt index 699f52575..8e3ad814a 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt @@ -20,9 +20,9 @@ import android.app.Application import coil.ImageLoader import coil.ImageLoaderFactory import com.google.samples.apps.nowinandroid.sync.initializers.Sync +import com.google.samples.apps.nowinandroid.util.ProfileVerifierLogger import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject -import javax.inject.Provider /** * [Application] class for NiA @@ -30,12 +30,16 @@ import javax.inject.Provider @HiltAndroidApp class NiaApplication : Application(), ImageLoaderFactory { @Inject - lateinit var imageLoader: Provider + lateinit var imageLoader: dagger.Lazy + + @Inject + lateinit var profileVerifierLogger: ProfileVerifierLogger override fun onCreate() { super.onCreate() // Initialize Sync; the system responsible for keeping data in the app up to date. Sync.initialize(context = this) + profileVerifierLogger() } override fun newImageLoader(): ImageLoader = imageLoader.get() diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/JankStatsModule.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/JankStatsModule.kt index be64d057f..56d1b6e24 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/JankStatsModule.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/JankStatsModule.kt @@ -20,6 +20,7 @@ import android.app.Activity import android.util.Log import android.view.Window import androidx.metrics.performance.JankStats +import androidx.metrics.performance.JankStats.OnFrameListener import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -29,26 +30,20 @@ import dagger.hilt.android.components.ActivityComponent @InstallIn(ActivityComponent::class) object JankStatsModule { @Provides - fun providesOnFrameListener(): JankStats.OnFrameListener { - return JankStats.OnFrameListener { frameData -> - // Make sure to only log janky frames. - if (frameData.isJank) { - // We're currently logging this but would better report it to a backend. - Log.v("NiA Jank", frameData.toString()) - } + fun providesOnFrameListener(): OnFrameListener = OnFrameListener { frameData -> + // Make sure to only log janky frames. + if (frameData.isJank) { + // We're currently logging this but would better report it to a backend. + Log.v("NiA Jank", frameData.toString()) } } @Provides - fun providesWindow(activity: Activity): Window { - return activity.window - } + fun providesWindow(activity: Activity): Window = activity.window @Provides fun providesJankStats( window: Window, - frameListener: JankStats.OnFrameListener, - ): JankStats { - return JankStats.createAndTrack(window, frameListener) - } + frameListener: OnFrameListener, + ): JankStats = JankStats.createAndTrack(window, frameListener) } 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 1660581a4..2beda99ea 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 @@ -97,9 +97,7 @@ fun NiaApp( ) { val shouldShowGradientBackground = appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU - var showSettingsDialog by rememberSaveable { - mutableStateOf(false) - } + var showSettingsDialog by rememberSaveable { mutableStateOf(false) } NiaBackground { NiaGradientBackground( 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 7a38b6649..7b66efb06 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 @@ -164,9 +164,7 @@ class NiaAppState( } } - fun navigateToSearch() { - navController.navigateToSearch() - } + fun navigateToSearch() = navController.navigateToSearch() } /** diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 6d5711aa5..77280bad5 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -20,10 +20,10 @@ android:viewportWidth="108" android:viewportHeight="108"> diff --git a/app/src/main/res/drawable/ic_splash.xml b/app/src/main/res/drawable/ic_splash.xml index 6de9c8c9b..144393be9 100644 --- a/app/src/main/res/drawable/ic_splash.xml +++ b/app/src/main/res/drawable/ic_splash.xml @@ -24,11 +24,11 @@ android:pathData="M0,0h108v108h-108z" android:fillColor="@color/ic_launcher_background_tint"/> - \ No newline at end of file + diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt index dcbc1e5c0..f7345f04f 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt @@ -37,6 +37,7 @@ import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepositor import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor +import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity import dagger.hilt.android.testing.BindValue @@ -140,13 +141,15 @@ class NiaAppScreenSizesScreenshotTests { ) { TestHarness(size = DpSize(width, height)) { BoxWithConstraints { - NiaApp( - windowSizeClass = WindowSizeClass.calculateFromSize( - DpSize(maxWidth, maxHeight), - ), - networkMonitor = networkMonitor, - userNewsResourceRepository = userNewsResourceRepository, - ) + NiaTheme { + NiaApp( + windowSizeClass = WindowSizeClass.calculateFromSize( + DpSize(maxWidth, maxHeight), + ), + networkMonitor = networkMonitor, + userNewsResourceRepository = userNewsResourceRepository, + ) + } } } } diff --git a/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png index edb9cfa2a..06db1c65b 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_compactHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png index 035cc24cf..fe9cd884a 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png index 7749199c5..5d75a44a7 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_mediumHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationRail.png index fe5b045aa..8568d174c 100644 Binary files a/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/expandedWidth_compactHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png index 523b03ec5..ab95819dd 100644 Binary files a/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png index 58d620f21..befeac749 100644 Binary files a/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/expandedWidth_mediumHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationRail.png index 56b49457c..57d5585da 100644 Binary files a/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/mediumWidth_compactHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png index 15ddccf78..e80a26617 100644 Binary files a/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png index d2e4bb8bc..0db65e2fb 100644 Binary files a/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/mediumWidth_mediumHeight_showsNavigationRail.png differ diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 67fccb979..279c4b226 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import com.google.samples.apps.nowinandroid.NiaBuildType import com.google.samples.apps.nowinandroid.configureFlavors plugins { @@ -35,23 +34,6 @@ android { buildConfig = true } - buildTypes { - // This benchmark buildType is used for benchmarking, and should function like your - // release build (for example, with minification on). It's signed with a debug key - // for easy local/CI testing. - create("benchmark") { - // Keep the build type debuggable so we can attach a debugger if needed. - isDebuggable = true - signingConfig = signingConfigs.getByName("debug") - matchingFallbacks.add("release") - buildConfigField( - "String", - "APP_BUILD_TYPE_SUFFIX", - "\"${NiaBuildType.BENCHMARK.applicationIdSuffix ?: ""}\"" - ) - } - } - // Use the same flavor dimensions as the application to allow generating Baseline Profiles on prod, // which is more close to what will be shipped to users (no fake data), but has ability to run the // benchmarks on demo, so we benchmark on stable data. diff --git a/benchmarks/src/main/kotlin/androidx/test/uiautomator/UiAutomatorHelpers.kt b/benchmarks/src/main/kotlin/androidx/test/uiautomator/UiAutomatorHelpers.kt index 85867b982..b0eb754c7 100644 --- a/benchmarks/src/main/kotlin/androidx/test/uiautomator/UiAutomatorHelpers.kt +++ b/benchmarks/src/main/kotlin/androidx/test/uiautomator/UiAutomatorHelpers.kt @@ -29,15 +29,11 @@ import androidx.test.uiautomator.HasChildrenOp.EXACTLY fun untilHasChildren( childCount: Int = 1, op: HasChildrenOp = AT_LEAST, -): UiObject2Condition { - return object : UiObject2Condition() { - override fun apply(element: UiObject2): Boolean { - return when (op) { - AT_LEAST -> element.childCount >= childCount - EXACTLY -> element.childCount == childCount - AT_MOST -> element.childCount <= childCount - } - } +): UiObject2Condition = object : UiObject2Condition() { + override fun apply(element: UiObject2): Boolean = when (op) { + AT_LEAST -> element.childCount >= childCount + EXACTLY -> element.childCount == childCount + AT_MOST -> element.childCount <= childCount } } diff --git a/benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/Utils.kt b/benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/Utils.kt index 9ece991c4..e8fb53c4f 100644 --- a/benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/Utils.kt +++ b/benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/Utils.kt @@ -30,7 +30,6 @@ import java.io.ByteArrayOutputStream val PACKAGE_NAME = buildString { append("com.google.samples.apps.nowinandroid") append(BuildConfig.APP_FLAVOR_SUFFIX) - append(BuildConfig.APP_BUILD_TYPE_SUFFIX) } fun UiDevice.flingElementDownUp(element: UiObject2) { diff --git a/benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/baselineprofile/StartupBaselineProfile.kt b/benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/baselineprofile/StartupBaselineProfile.kt index c5a88e1bd..d8128a670 100644 --- a/benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/baselineprofile/StartupBaselineProfile.kt +++ b/benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/baselineprofile/StartupBaselineProfile.kt @@ -16,6 +16,7 @@ package com.google.samples.apps.nowinandroid.baselineprofile +import androidx.benchmark.macro.MacrobenchmarkScope import androidx.benchmark.macro.junit4.BaselineProfileRule import com.google.samples.apps.nowinandroid.PACKAGE_NAME import com.google.samples.apps.nowinandroid.startActivityAndAllowNotifications @@ -30,11 +31,9 @@ class StartupBaselineProfile { @get:Rule val baselineProfileRule = BaselineProfileRule() @Test - fun generate() = - baselineProfileRule.collect( - PACKAGE_NAME, - includeInStartupProfile = true, - ) { - startActivityAndAllowNotifications() - } + fun generate() = baselineProfileRule.collect( + PACKAGE_NAME, + includeInStartupProfile = true, + profileBlock = MacrobenchmarkScope::startActivityAndAllowNotifications, + ) } diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 7a334beb3..b8699a05d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -44,6 +44,9 @@ 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.tracing.ktx").get()) + + add("androidTestImplementation", libs.findLibrary("androidx.lifecycle.runtimeTesting").get()) } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 8eb329f58..d442d94ef 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -21,6 +21,7 @@ import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configurePrintApksTask import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests +import com.google.samples.apps.nowinandroid.libs import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure @@ -51,6 +52,8 @@ class AndroidLibraryConventionPlugin : Plugin { } dependencies { add("testImplementation", kotlin("test")) + + add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) } } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaBuildType.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaBuildType.kt index 653506f51..e4f40840d 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaBuildType.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaBuildType.kt @@ -22,5 +22,4 @@ package com.google.samples.apps.nowinandroid enum class NiaBuildType(val applicationIdSuffix: String? = null) { DEBUG(".debug"), RELEASE, - BENCHMARK(".benchmark") } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/PrintTestApks.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/PrintTestApks.kt index 94bf6e127..8e88f5a53 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/PrintTestApks.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/PrintTestApks.kt @@ -86,14 +86,12 @@ internal abstract class PrintApkLocationTask : DefaultTask() { fun taskAction() { val hasFiles = sources.orNull?.any { directory -> directory.asFileTree.files.any { - it.isFile && it.parentFile.path.contains("build${File.separator}generated").not() + it.isFile && "build${File.separator}generated" !in it.parentFile.path } } ?: throw RuntimeException("Cannot check androidTest sources") // Don't print APK location if there are no androidTest source files - if (!hasFiles) { - return - } + if (!hasFiles) return val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get()) ?: throw RuntimeException("Cannot load APKs") diff --git a/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/result/Result.kt b/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/result/Result.kt index 6ae12d634..22376d082 100644 --- a/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/result/Result.kt +++ b/core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/result/Result.kt @@ -23,15 +23,10 @@ import kotlinx.coroutines.flow.onStart sealed interface Result { data class Success(val data: T) : Result - data class Error(val exception: Throwable? = null) : Result + data class Error(val exception: Throwable) : Result data object Loading : Result } -fun Flow.asResult(): Flow> { - return this - .map> { - Result.Success(it) - } - .onStart { emit(Result.Loading) } - .catch { emit(Result.Error(it)) } -} +fun Flow.asResult(): Flow> = map> { Result.Success(it) } + .onStart { emit(Result.Loading) } + .catch { emit(Result.Error(it)) } diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultRecentSearchRepository.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultRecentSearchRepository.kt index 2e5c4463b..32239362d 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultRecentSearchRepository.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultRecentSearchRepository.kt @@ -39,9 +39,7 @@ internal class DefaultRecentSearchRepository @Inject constructor( override fun getRecentSearchQueries(limit: Int): Flow> = recentSearchQueryDao.getRecentSearchQueryEntities(limit).map { searchQueries -> - searchQueries.map { - it.asExternalModel() - } + searchQueries.map { it.asExternalModel() } } override suspend fun clearRecentSearches() = recentSearchQueryDao.clearRecentSearchQueries() diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeRecentSearchRepository.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeRecentSearchRepository.kt index fc649f3ec..025b51f68 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeRecentSearchRepository.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeRecentSearchRepository.kt @@ -26,10 +26,10 @@ import javax.inject.Inject * Fake implementation of the [RecentSearchRepository] */ class FakeRecentSearchRepository @Inject constructor() : RecentSearchRepository { - override suspend fun insertOrReplaceRecentSearch(searchQuery: String) { /* no-op */ } + override suspend fun insertOrReplaceRecentSearch(searchQuery: String) = Unit override fun getRecentSearchQueries(limit: Int): Flow> = flowOf(emptyList()) - override suspend fun clearRecentSearches() { /* no-op */ } + override suspend fun clearRecentSearches() = Unit } diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeSearchContentsRepository.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeSearchContentsRepository.kt index d15890a10..65cced452 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeSearchContentsRepository.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeSearchContentsRepository.kt @@ -27,7 +27,7 @@ import javax.inject.Inject */ class FakeSearchContentsRepository @Inject constructor() : SearchContentsRepository { - override suspend fun populateFtsData() { /* no-op */ } + override suspend fun populateFtsData() = Unit override fun searchContents(searchQuery: String): Flow = flowOf() override fun getSearchContentsCount(): Flow = flowOf(1) } diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeTopicsRepository.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeTopicsRepository.kt index 1ab9c9353..0eefc8451 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeTopicsRepository.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/fake/FakeTopicsRepository.kt @@ -55,9 +55,8 @@ class FakeTopicsRepository @Inject constructor( ) }.flowOn(ioDispatcher) - override fun getTopic(id: String): Flow { - return getTopics().map { it.first { topic -> topic.id == id } } - } + override fun getTopic(id: String): Flow = getTopics() + .map { it.first { topic -> topic.id == id } } override suspend fun syncWith(synchronizer: Synchronizer) = true } diff --git a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt index 743fb7e5c..05811f4be 100644 --- a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt +++ b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt @@ -82,7 +82,7 @@ class CompositeUserNewsResourceRepositoryTest { // Check that only news resources with the given topic id are returned. assertEquals( sampleNewsResources - .filter { it.topics.contains(sampleTopic1) } + .filter { sampleTopic1 in it.topics } .mapToUserNewsResources(emptyUserData), userNewsResources.first(), ) @@ -104,7 +104,7 @@ class CompositeUserNewsResourceRepositoryTest { // Check that only news resources with the given topic id are returned. assertEquals( sampleNewsResources - .filter { it.topics.contains(sampleTopic1) } + .filter { sampleTopic1 in it.topics } .mapToUserNewsResources(userData), userNewsResources.first(), ) diff --git a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/UserNewsResourceTest.kt b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/UserNewsResourceTest.kt index a21dee863..c7dfd99d0 100644 --- a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/UserNewsResourceTest.kt +++ b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/UserNewsResourceTest.kt @@ -91,14 +91,14 @@ class UserNewsResourceTest { // Construct the expected FollowableTopic. val followableTopic = FollowableTopic( topic = topic, - isFollowed = userData.followedTopics.contains(topic.id), + isFollowed = topic.id in userData.followedTopics, ) assertTrue(userNewsResource.followableTopics.contains(followableTopic)) } // Check that the saved flag is set correctly. assertEquals( - userData.bookmarkedNewsResources.contains(newsResource1.id), + newsResource1.id in userData.bookmarkedNewsResources, userNewsResource.isSaved, ) } diff --git a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt index 6e5c45305..dc4b78e01 100644 --- a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt +++ b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt @@ -34,9 +34,7 @@ val nonPresentInterestsIds = setOf("2") */ class TestNewsResourceDao : NewsResourceDao { - private var entitiesStateFlow = MutableStateFlow( - emptyList(), - ) + private val entitiesStateFlow = MutableStateFlow(emptyList()) internal var topicCrossReferences: List = listOf() @@ -131,7 +129,7 @@ class TestNewsResourceDao : NewsResourceDao { override suspend fun deleteNewsResources(ids: List) { val idSet = ids.toSet() entitiesStateFlow.update { entities -> - entities.filterNot { idSet.contains(it.id) } + entities.filterNot { it.id in idSet } } } } diff --git a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNiaNetworkDataSource.kt b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNiaNetworkDataSource.kt index 8e248a3aa..7f9a69959 100644 --- a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNiaNetworkDataSource.kt +++ b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNiaNetworkDataSource.kt @@ -91,11 +91,10 @@ class TestNiaNetworkDataSource : NiaNetworkDataSource { } } -fun List.after(version: Int?): List = - when (version) { - null -> this - else -> this.filter { it.changeListVersion > version } - } +fun List.after(version: Int?): List = when (version) { + null -> this + else -> filter { it.changeListVersion > version } +} /** * Return items from [this] whose id defined by [idGetter] is in [ids] if [ids] is not null @@ -105,7 +104,7 @@ private fun List.matchIds( idGetter: (T) -> String, ) = when (ids) { null -> this - else -> ids.toSet().let { idSet -> this.filter { idSet.contains(idGetter(it)) } } + else -> ids.toSet().let { idSet -> filter { idGetter(it) in idSet } } } /** diff --git a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestTopicDao.kt b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestTopicDao.kt index a52cc86f6..d217f55d7 100644 --- a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestTopicDao.kt +++ b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestTopicDao.kt @@ -28,20 +28,15 @@ import kotlinx.coroutines.flow.update */ class TestTopicDao : TopicDao { - private var entitiesStateFlow = MutableStateFlow( - emptyList(), - ) + private val entitiesStateFlow = MutableStateFlow(emptyList()) - override fun getTopicEntity(topicId: String): Flow { + override fun getTopicEntity(topicId: String): Flow = throw NotImplementedError("Unused in tests") - } - override fun getTopicEntities(): Flow> = - entitiesStateFlow + override fun getTopicEntities(): Flow> = entitiesStateFlow override fun getTopicEntities(ids: Set): Flow> = - getTopicEntities() - .map { topics -> topics.filter { it.id in ids } } + getTopicEntities().map { topics -> topics.filter { it.id in ids } } override suspend fun getOneOffTopicEntities(): List = emptyList() @@ -55,15 +50,11 @@ class TestTopicDao : TopicDao { override suspend fun upsertTopics(entities: List) { // Overwrite old values with new values - entitiesStateFlow.update { oldValues -> - (entities + oldValues).distinctBy(TopicEntity::id) - } + entitiesStateFlow.update { oldValues -> (entities + oldValues).distinctBy(TopicEntity::id) } } override suspend fun deleteTopics(ids: List) { val idSet = ids.toSet() - entitiesStateFlow.update { entities -> - entities.filterNot { idSet.contains(it.id) } - } + entitiesStateFlow.update { entities -> entities.filterNot { it.id in idSet } } } } diff --git a/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigration.kt b/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigration.kt index faa491e74..5675aee05 100644 --- a/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigration.kt +++ b/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/ListToMapMigration.kt @@ -52,7 +52,6 @@ internal object ListToMapMigration : DataMigration { hasDoneListToMapMigration = true } - override suspend fun shouldMigrate(currentData: UserPreferences): Boolean { - return !currentData.hasDoneListToMapMigration - } + override suspend fun shouldMigrate(currentData: UserPreferences): Boolean = + !currentData.hasDoneListToMapMigration } diff --git a/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt b/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt index 6dc7725c1..9a76a75a1 100644 --- a/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt +++ b/core/datastore/src/main/kotlin/com/google/samples/apps/nowinandroid/core/datastore/NiaPreferencesDataSource.kt @@ -103,9 +103,7 @@ class NiaPreferencesDataSource @Inject constructor( suspend fun setDynamicColorPreference(useDynamicColor: Boolean) { userPreferences.updateData { - it.copy { - this.useDynamicColor = useDynamicColor - } + it.copy { this.useDynamicColor = useDynamicColor } } } @@ -190,9 +188,7 @@ class NiaPreferencesDataSource @Inject constructor( suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) { userPreferences.updateData { - it.copy { - this.shouldHideOnboarding = shouldHideOnboarding - } + it.copy { this.shouldHideOnboarding = shouldHideOnboarding } } } } diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt index bd22fa168..1557cac06 100644 --- a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/DynamicAsyncImage.kt @@ -28,6 +28,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.graphics.Color.Companion.Unspecified import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale @@ -79,7 +80,7 @@ fun DynamicAsyncImage( contentScale = ContentScale.Crop, painter = if (isError.not() && !isLocalInspection) imageLoader else placeholder, contentDescription = contentDescription, - colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, + colorFilter = if (iconTint != Unspecified) ColorFilter.tint(iconTint) else null, ) } } diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/TopAppBar.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/TopAppBar.kt index 99f935d2a..9c716918a 100644 --- a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/TopAppBar.kt +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/TopAppBar.kt @@ -40,9 +40,9 @@ import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons fun NiaTopAppBar( @StringRes titleRes: Int, navigationIcon: ImageVector, - navigationIconContentDescription: String?, + navigationIconContentDescription: String, actionIcon: ImageVector, - actionIconContentDescription: String?, + actionIconContentDescription: String, modifier: Modifier = Modifier, colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(), onNavigationClick: () -> Unit = {}, diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt index a55f62f5f..3fcc8f2c0 100644 --- a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt @@ -230,10 +230,5 @@ fun LazyStaggeredGridState.scrollbarState( return state } -private inline fun List.floatSumOf(selector: (T) -> Float): Float { - var sum = 0f - for (element in this) { - sum += selector(element) - } - return sum -} +private inline fun List.floatSumOf(selector: (T) -> Float): Float = + fold(initial = 0f) { accumulator, listItem -> accumulator + selector(listItem) } diff --git a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/theme/Tint.kt b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/theme/Tint.kt index 848c8d8f5..75ab3a8f6 100644 --- a/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/theme/Tint.kt +++ b/core/designsystem/src/main/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/theme/Tint.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.graphics.Color */ @Immutable data class TintTheme( - val iconTint: Color? = null, + val iconTint: Color = Color.Unspecified, ) /** diff --git a/core/designsystem/src/main/res/drawable/core_designsystem_ic_placeholder_default.xml b/core/designsystem/src/main/res/drawable/core_designsystem_ic_placeholder_default.xml index a00c2de22..f5d87a103 100644 --- a/core/designsystem/src/main/res/drawable/core_designsystem_ic_placeholder_default.xml +++ b/core/designsystem/src/main/res/drawable/core_designsystem_ic_placeholder_default.xml @@ -32,12 +32,12 @@ android:fillColor="#8C4190" android:fillAlpha="0.11"/> diff --git a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt index eb49cc374..1044a5443 100644 --- a/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt +++ b/core/designsystem/src/test/kotlin/com/google/samples/apps/nowinandroid/core/designsystem/ThemeTest.kt @@ -16,7 +16,8 @@ package com.google.samples.apps.nowinandroid.core.designsystem -import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.dynamicDarkColorScheme @@ -222,60 +223,41 @@ class ThemeTest { } @Composable - private fun dynamicLightColorSchemeWithFallback(): ColorScheme { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - dynamicLightColorScheme(LocalContext.current) - } else { - LightDefaultColorScheme - } + private fun dynamicLightColorSchemeWithFallback(): ColorScheme = when { + SDK_INT >= VERSION_CODES.S -> dynamicLightColorScheme(LocalContext.current) + else -> LightDefaultColorScheme } @Composable - private fun dynamicDarkColorSchemeWithFallback(): ColorScheme { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - dynamicDarkColorScheme(LocalContext.current) - } else { - DarkDefaultColorScheme - } + private fun dynamicDarkColorSchemeWithFallback(): ColorScheme = when { + SDK_INT >= VERSION_CODES.S -> dynamicDarkColorScheme(LocalContext.current) + else -> DarkDefaultColorScheme } - private fun emptyGradientColors(colorScheme: ColorScheme): GradientColors { - return GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp)) - } + private fun emptyGradientColors(colorScheme: ColorScheme): GradientColors = + GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp)) - private fun defaultGradientColors(colorScheme: ColorScheme): GradientColors { - return GradientColors( - top = colorScheme.inverseOnSurface, - bottom = colorScheme.primaryContainer, - container = colorScheme.surface, - ) - } + private fun defaultGradientColors(colorScheme: ColorScheme): GradientColors = GradientColors( + top = colorScheme.inverseOnSurface, + bottom = colorScheme.primaryContainer, + container = colorScheme.surface, + ) - private fun dynamicGradientColorsWithFallback(colorScheme: ColorScheme): GradientColors { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - emptyGradientColors(colorScheme) - } else { - defaultGradientColors(colorScheme) - } + private fun dynamicGradientColorsWithFallback(colorScheme: ColorScheme): GradientColors = when { + SDK_INT >= VERSION_CODES.S -> emptyGradientColors(colorScheme) + else -> defaultGradientColors(colorScheme) } - private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme { - return BackgroundTheme( - color = colorScheme.surface, - tonalElevation = 2.dp, - ) - } + private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme = BackgroundTheme( + color = colorScheme.surface, + tonalElevation = 2.dp, + ) - private fun defaultTintTheme(): TintTheme { - return TintTheme() - } + private fun defaultTintTheme(): TintTheme = TintTheme() - private fun dynamicTintThemeWithFallback(colorScheme: ColorScheme): TintTheme { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - TintTheme(colorScheme.primary) - } else { - TintTheme() - } + private fun dynamicTintThemeWithFallback(colorScheme: ColorScheme): TintTheme = when { + SDK_INT >= VERSION_CODES.S -> TintTheme(colorScheme.primary) + else -> TintTheme() } /** diff --git a/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt b/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt index c3c045d44..0167a3192 100644 --- a/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt +++ b/core/domain/src/main/kotlin/com/google/samples/apps/nowinandroid/core/domain/GetFollowableTopicsUseCase.kt @@ -37,22 +37,20 @@ class GetFollowableTopicsUseCase @Inject constructor( * * @param sortBy - the field used to sort the topics. Default NONE = no sorting. */ - operator fun invoke(sortBy: TopicSortField = NONE): Flow> { - return combine( - userDataRepository.userData, - topicsRepository.getTopics(), - ) { userData, topics -> - val followedTopics = topics - .map { topic -> - FollowableTopic( - topic = topic, - isFollowed = topic.id in userData.followedTopics, - ) - } - when (sortBy) { - NAME -> followedTopics.sortedBy { it.topic.name } - else -> followedTopics + operator fun invoke(sortBy: TopicSortField = NONE): Flow> = combine( + userDataRepository.userData, + topicsRepository.getTopics(), + ) { userData, topics -> + val followedTopics = topics + .map { topic -> + FollowableTopic( + topic = topic, + isFollowed = topic.id in userData.followedTopics, + ) } + when (sortBy) { + NAME -> followedTopics.sortedBy { it.topic.name } + else -> followedTopics } } } 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 4ea830022..a56bbcb8d 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 @@ -45,14 +45,13 @@ data class UserNewsResource internal constructor( followableTopics = newsResource.topics.map { topic -> FollowableTopic( topic = topic, - isFollowed = userData.followedTopics.contains(topic.id), + isFollowed = topic.id in userData.followedTopics, ) }, - isSaved = userData.bookmarkedNewsResources.contains(newsResource.id), - hasBeenViewed = userData.viewedNewsResources.contains(newsResource.id), + isSaved = newsResource.id in userData.bookmarkedNewsResources, + hasBeenViewed = newsResource.id in userData.viewedNewsResources, ) } -fun List.mapToUserNewsResources(userData: UserData): List { - return map { UserNewsResource(it, userData) } -} +fun List.mapToUserNewsResources(userData: UserData): List = + map { UserNewsResource(it, userData) } diff --git a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt index 21d93c0e4..a68683c7c 100644 --- a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/di/NetworkModule.kt @@ -17,6 +17,7 @@ package com.google.samples.apps.nowinandroid.core.network.di import android.content.Context +import androidx.tracing.trace import coil.ImageLoader import coil.decode.SvgDecoder import coil.util.DebugLogger @@ -51,16 +52,18 @@ internal object NetworkModule { @Provides @Singleton - fun okHttpCallFactory(): Call.Factory = OkHttpClient.Builder() - .addInterceptor( - HttpLoggingInterceptor() - .apply { - if (BuildConfig.DEBUG) { - setLevel(HttpLoggingInterceptor.Level.BODY) - } - }, - ) - .build() + fun okHttpCallFactory(): Call.Factory = trace("NiaOkHttpClient") { + OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor() + .apply { + if (BuildConfig.DEBUG) { + setLevel(HttpLoggingInterceptor.Level.BODY) + } + }, + ) + .build() + } /** * Since we're displaying SVGs in the app, Coil needs an ImageLoader which supports this @@ -72,20 +75,21 @@ internal object NetworkModule { @Provides @Singleton fun imageLoader( - okHttpCallFactory: Call.Factory, + // We specifically request dagger.Lazy here, so that it's not instantiated from Dagger. + okHttpCallFactory: dagger.Lazy, @ApplicationContext application: Context, - ): ImageLoader = ImageLoader.Builder(application) - .callFactory(okHttpCallFactory) - .components { - add(SvgDecoder.Factory()) - } - // Assume most content images are versioned urls - // but some problematic images are fetching each time - .respectCacheHeaders(false) - .apply { - if (BuildConfig.DEBUG) { - logger(DebugLogger()) + ): ImageLoader = trace("NiaImageLoader") { + ImageLoader.Builder(application) + .callFactory { okHttpCallFactory.get() } + .components { add(SvgDecoder.Factory()) } + // Assume most content images are versioned urls + // but some problematic images are fetching each time + .respectCacheHeaders(false) + .apply { + if (BuildConfig.DEBUG) { + logger(DebugLogger()) + } } - } - .build() + .build() + } } diff --git a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/retrofit/RetrofitNiaNetwork.kt b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/retrofit/RetrofitNiaNetwork.kt index 321e856fe..e9fe99d9e 100644 --- a/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/retrofit/RetrofitNiaNetwork.kt +++ b/core/network/src/main/kotlin/com/google/samples/apps/nowinandroid/core/network/retrofit/RetrofitNiaNetwork.kt @@ -16,6 +16,7 @@ package com.google.samples.apps.nowinandroid.core.network.retrofit +import androidx.tracing.trace import com.google.samples.apps.nowinandroid.core.network.BuildConfig import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList @@ -73,17 +74,21 @@ private data class NetworkResponse( @Singleton internal class RetrofitNiaNetwork @Inject constructor( networkJson: Json, - okhttpCallFactory: Call.Factory, + okhttpCallFactory: dagger.Lazy, ) : NiaNetworkDataSource { - private val networkApi = Retrofit.Builder() - .baseUrl(NIA_BASE_URL) - .callFactory(okhttpCallFactory) - .addConverterFactory( - networkJson.asConverterFactory("application/json".toMediaType()), - ) - .build() - .create(RetrofitNiaNetworkApi::class.java) + private val networkApi = trace("RetrofitNiaNetwork") { + Retrofit.Builder() + .baseUrl(NIA_BASE_URL) + // We use callFactory lambda here with dagger.Lazy + // to prevent initializing OkHttp on the main thread. + .callFactory { okhttpCallFactory.get().newCall(it) } + .addConverterFactory( + networkJson.asConverterFactory("application/json".toMediaType()), + ) + .build() + .create(RetrofitNiaNetworkApi::class.java) + } override suspend fun getTopics(ids: List?): List = networkApi.getTopics(ids = ids).data 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 ebad9fda3..1c9e7ab63 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 @@ -24,10 +24,10 @@ import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.pm.PackageManager +import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build.VERSION import android.os.Build.VERSION_CODES -import androidx.core.app.ActivityCompat +import androidx.core.app.ActivityCompat.checkSelfPermission import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.InboxStyle import androidx.core.app.NotificationManagerCompat @@ -57,30 +57,24 @@ internal class SystemTrayNotifier @Inject constructor( override fun postNewsNotifications( newsResources: List, ) = with(context) { - if (ActivityCompat.checkSelfPermission( - this, - permission.POST_NOTIFICATIONS, - ) != PackageManager.PERMISSION_GRANTED - ) { + if (checkSelfPermission(this, permission.POST_NOTIFICATIONS) != PERMISSION_GRANTED) { return } - val truncatedNewsResources = newsResources - .take(MAX_NUM_NOTIFICATIONS) + val truncatedNewsResources = newsResources.take(MAX_NUM_NOTIFICATIONS) - val newsNotifications = truncatedNewsResources - .map { newsResource -> - createNewsNotification { - setSmallIcon( - com.google.samples.apps.nowinandroid.core.common.R.drawable.core_common_ic_nia_notification, - ) - .setContentTitle(newsResource.title) - .setContentText(newsResource.content) - .setContentIntent(newsPendingIntent(newsResource)) - .setGroup(NEWS_NOTIFICATION_GROUP) - .setAutoCancel(true) - } + val newsNotifications = truncatedNewsResources.map { newsResource -> + createNewsNotification { + setSmallIcon( + com.google.samples.apps.nowinandroid.core.common.R.drawable.core_common_ic_nia_notification, + ) + .setContentTitle(newsResource.title) + .setContentText(newsResource.content) + .setContentIntent(newsPendingIntent(newsResource)) + .setGroup(NEWS_NOTIFICATION_GROUP) + .setAutoCancel(true) } + } val summaryNotification = createNewsNotification { val title = getString( R.string.core_notifications_news_notification_group_summary, @@ -117,9 +111,7 @@ internal class SystemTrayNotifier @Inject constructor( newsResources: List, title: String, ): InboxStyle = newsResources - .fold(InboxStyle()) { inboxStyle, newsResource -> - inboxStyle.addLine(newsResource.title) - } + .fold(InboxStyle()) { inboxStyle, newsResource -> inboxStyle.addLine(newsResource.title) } .setBigContentTitle(title) .setSummaryText(title) } diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/NiaTestRunner.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/NiaTestRunner.kt index 30254a617..9b3b185df 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/NiaTestRunner.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/NiaTestRunner.kt @@ -25,7 +25,6 @@ import dagger.hilt.android.testing.HiltTestApplication * A custom runner to set up the instrumented application class for tests. */ class NiaTestRunner : AndroidJUnitRunner() { - override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { - return super.newApplication(cl, HiltTestApplication::class.java.name, context) - } + override fun newApplication(cl: ClassLoader, name: String, context: Context): Application = + super.newApplication(cl, HiltTestApplication::class.java.name, context) } diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestNewsRepository.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestNewsRepository.kt index d0bfd21a1..ef065a9f8 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestNewsRepository.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestNewsRepository.kt @@ -43,9 +43,7 @@ class TestNewsRepository : NewsRepository { } } query.filterNewsIds?.let { filterNewsIds -> - result = newsResources.filter { - filterNewsIds.contains(it.id) - } + result = newsResources.filter { it.id in filterNewsIds } } result } diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestRecentSearchRepository.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestRecentSearchRepository.kt index 961473401..f700bdc31 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestRecentSearchRepository.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestRecentSearchRepository.kt @@ -32,7 +32,5 @@ class TestRecentSearchRepository : RecentSearchRepository { cachedRecentSearches.add(RecentSearchQuery(searchQuery)) } - override suspend fun clearRecentSearches() { - cachedRecentSearches.clear() - } + override suspend fun clearRecentSearches() = cachedRecentSearches.clear() } diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestSearchContentsRepository.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestSearchContentsRepository.kt index 2aa54e463..5436cd10f 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestSearchContentsRepository.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestSearchContentsRepository.kt @@ -21,45 +21,36 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.SearchResult import com.google.samples.apps.nowinandroid.core.model.data.Topic import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import org.jetbrains.annotations.TestOnly class TestSearchContentsRepository : SearchContentsRepository { - private val cachedTopics: MutableList = mutableListOf() - private val cachedNewsResources: MutableList = mutableListOf() + private val cachedTopics = MutableStateFlow(emptyList()) + private val cachedNewsResources = MutableStateFlow(emptyList()) - override suspend fun populateFtsData() { /* no-op */ } + override suspend fun populateFtsData() = Unit - override fun searchContents(searchQuery: String): Flow = flowOf( - SearchResult( - topics = cachedTopics.filter { - it.name.contains(searchQuery) || - it.shortDescription.contains(searchQuery) || - it.longDescription.contains(searchQuery) - }, - newsResources = cachedNewsResources.filter { - it.content.contains(searchQuery) || - it.title.contains(searchQuery) - }, - ), - ) + override fun searchContents(searchQuery: String): Flow = + combine(cachedTopics, cachedNewsResources) { topics, news -> + SearchResult( + topics = topics.filter { + searchQuery in it.name || searchQuery in it.shortDescription || searchQuery in it.longDescription + }, + newsResources = news.filter { + searchQuery in it.content || searchQuery in it.title + }, + ) + } - override fun getSearchContentsCount(): Flow = flow { - emit(cachedTopics.size + cachedNewsResources.size) - } + override fun getSearchContentsCount(): Flow = combine(cachedTopics, cachedNewsResources) { topics, news -> topics.size + news.size } - /** - * Test only method to add the topics to the stored list in memory - */ - fun addTopics(topics: List) { - cachedTopics.addAll(topics) - } + @TestOnly + fun addTopics(topics: List) = cachedTopics.update { it + topics } - /** - * Test only method to add the news resources to the stored list in memory - */ - fun addNewsResources(newsResources: List) { - cachedNewsResources.addAll(newsResources) - } + @TestOnly + fun addNewsResources(newsResources: List) = + cachedNewsResources.update { it + newsResources } } diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestTopicsRepository.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestTopicsRepository.kt index a95469d83..ddccbbe35 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestTopicsRepository.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/repository/TestTopicsRepository.kt @@ -33,9 +33,8 @@ class TestTopicsRepository : TopicsRepository { override fun getTopics(): Flow> = topicsFlow - override fun getTopic(id: String): Flow { - return topicsFlow.map { topics -> topics.find { it.id == id }!! } - } + override fun getTopic(id: String): Flow = + topicsFlow.map { topics -> topics.find { it.id == id }!! } /** * A test-only API to allow controlling the list of topics from tests. diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/MainDispatcherRule.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/MainDispatcherRule.kt index 28155f5ad..666c4edd4 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/MainDispatcherRule.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/MainDispatcherRule.kt @@ -32,11 +32,7 @@ import org.junit.runner.Description class MainDispatcherRule( private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { - override fun starting(description: Description) { - Dispatchers.setMain(testDispatcher) - } + override fun starting(description: Description) = Dispatchers.setMain(testDispatcher) - override fun finished(description: Description) { - Dispatchers.resetMain() - } + override fun finished(description: Description) = Dispatchers.resetMain() } diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestAnalyticsHelper.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestAnalyticsHelper.kt index 005784c21..5f72d30e6 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestAnalyticsHelper.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestAnalyticsHelper.kt @@ -26,5 +26,5 @@ class TestAnalyticsHelper : AnalyticsHelper { events.add(event) } - fun hasLogged(event: AnalyticsEvent) = events.contains(event) + fun hasLogged(event: AnalyticsEvent) = event in events } diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestSyncManager.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestSyncManager.kt index 999b67195..ff1e2fdd9 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestSyncManager.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestSyncManager.kt @@ -26,9 +26,7 @@ class TestSyncManager : SyncManager { override val isSyncing: Flow = syncStatusFlow - override fun requestSync() { - TODO("Not yet implemented") - } + override fun requestSync(): Unit = TODO("Not yet implemented") /** * A test-only API to set the sync status from tests. diff --git a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt index c9fee1ac8..ef3de1059 100644 --- a/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt +++ b/core/ui/src/main/kotlin/com/google/samples/apps/nowinandroid/core/ui/JankStatsExtensions.kt @@ -50,7 +50,7 @@ fun rememberMetricsStateHolder(): Holder { */ @Composable fun TrackJank( - vararg keys: Any?, + vararg keys: Any, reportMetric: suspend CoroutineScope.(state: Holder) -> Unit, ) { val metrics = rememberMetricsStateHolder() @@ -65,7 +65,7 @@ fun TrackJank( */ @Composable fun TrackDisposableJank( - vararg keys: Any?, + vararg keys: Any, reportMetric: DisposableEffectScope.(state: Holder) -> DisposableEffectResult, ) { val metrics = rememberMetricsStateHolder() diff --git a/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt index 3d684f9d1..40f54e4a7 100644 --- a/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt +++ b/feature/bookmarks/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt @@ -17,6 +17,8 @@ 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 @@ -30,8 +32,11 @@ import androidx.compose.ui.test.onNodeWithContentDescription 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.testing.TestLifecycleOwner import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals @@ -166,4 +171,29 @@ class BookmarksScreenTest { ) .assertExists() } + + @Test + fun feed_whenLifecycleStops_undoBookmarkedStateIsCleared() = runTest { + var undoStateCleared = false + val testLifecycleOwner = TestLifecycleOwner(initialState = Lifecycle.State.STARTED) + + composeTestRule.setContent { + CompositionLocalProvider(LocalLifecycleOwner provides testLifecycleOwner) { + BookmarksScreen( + feedState = NewsFeedUiState.Success(emptyList()), + onShowSnackbar = { _, _ -> false }, + removeFromBookmarks = {}, + onTopicClick = {}, + onNewsResourceViewed = {}, + clearUndoState = { + undoStateCleared = true + }, + ) + } + } + + assertEquals(false, undoStateCleared) + testLifecycleOwner.handleLifecycleEvent(event = Lifecycle.Event.ON_STOP) + assertEquals(true, undoStateCleared) + } } diff --git a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index d316b0bfd..7c229c5ea 100644 --- a/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -42,13 +42,12 @@ import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridS import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -59,7 +58,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar @@ -127,15 +126,8 @@ internal fun BookmarksScreen( } } - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_STOP) { - clearUndoState() - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { + clearUndoState() } when (feedState) { @@ -237,7 +229,7 @@ private fun EmptyState(modifier: Modifier = Modifier) { Image( modifier = Modifier.fillMaxWidth(), painter = painterResource(id = R.drawable.feature_bookmarks_img_empty_bookmarks), - colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, + colorFilter = if (iconTint != Color.Unspecified) ColorFilter.tint(iconTint) else null, contentDescription = null, ) 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 81fa114e2..13d0baef0 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 @@ -24,9 +24,7 @@ import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute const val BOOKMARKS_ROUTE = "bookmarks_route" -fun NavController.navigateToBookmarks(navOptions: NavOptions? = null) { - this.navigate(BOOKMARKS_ROUTE, navOptions) -} +fun NavController.navigateToBookmarks(navOptions: NavOptions) = navigate(BOOKMARKS_ROUTE, navOptions) fun NavGraphBuilder.bookmarksScreen( onTopicClick: (String) -> Unit, diff --git a/feature/foryou/build.gradle.kts b/feature/foryou/build.gradle.kts index 3f990b902..da0a7ec5b 100644 --- a/feature/foryou/build.gradle.kts +++ b/feature/foryou/build.gradle.kts @@ -18,6 +18,7 @@ plugins { alias(libs.plugins.nowinandroid.android.feature) alias(libs.plugins.nowinandroid.android.library.compose) alias(libs.plugins.nowinandroid.android.library.jacoco) + alias(libs.plugins.roborazzi) } android { @@ -32,6 +33,7 @@ dependencies { testImplementation(libs.hilt.android.testing) testImplementation(libs.robolectric) testImplementation(projects.core.testing) + testDemoImplementation(libs.roborazzi) androidTestImplementation(projects.core.testing) } 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 30134715b..e1418d747 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 @@ -81,9 +81,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.unit.sp -import androidx.compose.ui.util.trace import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.tracing.trace import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus.Denied import com.google.accompanist.permissions.rememberPermissionState 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 154b0f83b..8e94a491a 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 @@ -30,9 +30,7 @@ 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}" -fun NavController.navigateToForYou(navOptions: NavOptions? = null) { - this.navigate(FOR_YOU_ROUTE, navOptions) -} +fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(FOR_YOU_ROUTE, navOptions) fun NavGraphBuilder.forYouScreen(onTopicClick: (String) -> Unit) { composable( diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png index deb0cd855..7d01a13eb 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_foldable.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png index 579bc98a8..e03dd3450 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png index f013bb40a..a0e2c9e10 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_phone_dark.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png index 75d6bc066..2c294af94 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedAndLoading_tablet.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png index a12c429d9..d8a99e736 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_foldable.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png index 0c36a8913..708cd5107 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_phone.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png index 021958401..098a7801d 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png and b/feature/foryou/src/test/screenshots/ForYouScreenPopulatedFeed_tablet.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_foldable.png b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_foldable.png index 715889be5..3f2a9e6ff 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_foldable.png and b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_foldable.png differ diff --git a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_tablet.png b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_tablet.png index 9a51764c5..58d898dcf 100644 Binary files a/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_tablet.png and b/feature/foryou/src/test/screenshots/ForYouScreenTopicSelection_tablet.png differ 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 831247e27..2ad7c560b 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 @@ -26,9 +26,7 @@ import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute private const val INTERESTS_GRAPH_ROUTE_PATTERN = "interests_graph" const val INTERESTS_ROUTE = "interests_route" -fun NavController.navigateToInterestsGraph(navOptions: NavOptions? = null) { - this.navigate(INTERESTS_GRAPH_ROUTE_PATTERN, navOptions) -} +fun NavController.navigateToInterestsGraph(navOptions: NavOptions) = navigate(INTERESTS_GRAPH_ROUTE_PATTERN, navOptions) fun NavGraphBuilder.interestsGraph( onTopicClick: (String) -> Unit, 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 95e5514fe..d05f02b22 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 @@ -264,9 +264,7 @@ fun EmptySearchResultBody( ) { offset -> tryAnotherSearchString.getStringAnnotations(start = offset, end = offset) .firstOrNull() - ?.let { - onInterestsClick() - } + ?.let { onInterestsClick() } } } } @@ -520,9 +518,7 @@ private fun SearchTextField( } }, onValueChange = { - if (!it.contains("\n")) { - onSearchQueryChanged(it) - } + if ("\n" !in it) onSearchQueryChanged(it) }, modifier = Modifier .fillMaxWidth() diff --git a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/navigation/SearchNavigation.kt b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/navigation/SearchNavigation.kt index a449600b2..81f3576b4 100644 --- a/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/navigation/SearchNavigation.kt +++ b/feature/search/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/search/navigation/SearchNavigation.kt @@ -24,9 +24,7 @@ import com.google.samples.apps.nowinandroid.feature.search.SearchRoute const val SEARCH_ROUTE = "search_route" -fun NavController.navigateToSearch(navOptions: NavOptions? = null) { - this.navigate(SEARCH_ROUTE, navOptions) -} +fun NavController.navigateToSearch(navOptions: NavOptions? = null) = navigate(SEARCH_ROUTE, navOptions) fun NavGraphBuilder.searchScreen( onBackClick: () -> Unit, diff --git a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt b/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt index fc9c20549..da0d5654e 100644 --- a/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt +++ b/feature/search/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/search/SearchViewModelTest.kt @@ -26,6 +26,7 @@ import com.google.samples.apps.nowinandroid.core.testing.data.topicsTestData import com.google.samples.apps.nowinandroid.core.testing.repository.TestRecentSearchRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestSearchContentsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository +import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.feature.search.RecentSearchQueriesUiState.Success import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.EmptyQuery @@ -71,6 +72,7 @@ class SearchViewModelTest { recentSearchRepository = recentSearchRepository, analyticsHelper = NoOpAnalyticsHelper(), ) + userDataRepository.setUserData(emptyUserData) } @Test @@ -100,8 +102,7 @@ class SearchViewModelTest { searchContentsRepository.addTopics(topicsTestData) val result = viewModel.searchResultUiState.value - // TODO: Figure out to get the latest emitted ui State? The result is emitted as EmptyQuery - // assertIs(result) + assertIs(result) collectJob.cancel() } 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 6adfe0a67..ff1eee319 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 @@ -117,22 +117,16 @@ private fun topicUiState( when (followedTopicToTopicResult) { is Result.Success -> { val (followedTopics, topic) = followedTopicToTopicResult.data - val followed = followedTopics.contains(topicId) TopicUiState.Success( followableTopic = FollowableTopic( topic = topic, - isFollowed = followed, + isFollowed = topicId in followedTopics, ), ) } - is Result.Loading -> { - TopicUiState.Loading - } - - is Result.Error -> { - TopicUiState.Error - } + is Result.Loading -> TopicUiState.Loading + is Result.Error -> TopicUiState.Error } } } @@ -151,26 +145,13 @@ private fun newsUiState( val bookmark: Flow> = userDataRepository.userData .map { it.bookmarkedNewsResources } - return combine( - newsStream, - bookmark, - ::Pair, - ) + return combine(newsStream, bookmark, ::Pair) .asResult() .map { newsToBookmarksResult -> when (newsToBookmarksResult) { - is Result.Success -> { - val news = newsToBookmarksResult.data.first - NewsUiState.Success(news) - } - - is Result.Loading -> { - NewsUiState.Loading - } - - is Result.Error -> { - NewsUiState.Error - } + is Result.Success -> NewsUiState.Success(newsToBookmarksResult.data.first) + is Result.Loading -> NewsUiState.Loading + is Result.Error -> NewsUiState.Error } } } 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 8052f766e..bba46c5ab 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 @@ -40,7 +40,7 @@ internal class TopicArgs(val topicId: String) { fun NavController.navigateToTopic(topicId: String) { val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING) - this.navigate("topic_route/$encodedId") { + navigate("topic_route/$encodedId") { launchSingleTop = true } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 795510bce..7e0380cab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.0.0" androidxEspresso = "3.5.1" androidxHiltNavigationCompose = "1.0.0" -androidxLifecycle = "2.6.2" +androidxLifecycle = "2.7.0" androidxMacroBenchmark = "1.2.2" androidxMetrics = "1.0.0-alpha04" androidxNavigation = "2.7.4" @@ -24,7 +24,7 @@ androidxTestCore = "1.5.0" androidxTestExt = "1.1.5" androidxTestRules = "1.5.0" androidxTestRunner = "1.5.2" -androidxTracing = "1.1.0" +androidxTracing = "1.3.0-alpha02" androidxUiAutomator = "2.2.0" androidxWindowManager = "1.2.0" androidxWork = "2.9.0" @@ -46,7 +46,7 @@ kotlinxDatetime = "0.5.0" kotlinxSerializationJson = "1.6.0" ksp = "1.9.21-1.0.16" okhttp = "4.12.0" -protobuf = "3.24.4" +protobuf = "3.25.2" protobufPlugin = "0.9.4" retrofit = "2.9.0" retrofitKotlinxSerializationJson = "1.0.0" @@ -82,6 +82,7 @@ androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscree androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } +androidx-lifecycle-runtimeTesting = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } diff --git a/lint/src/main/kotlin/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemDetector.kt b/lint/src/main/kotlin/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemDetector.kt index 4c9d55764..09af17db9 100644 --- a/lint/src/main/kotlin/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemDetector.kt +++ b/lint/src/main/kotlin/com/google/samples/apps/nowinandroid/lint/designsystem/DesignSystemDetector.kt @@ -34,15 +34,13 @@ import org.jetbrains.uast.UQualifiedReferenceExpression */ class DesignSystemDetector : Detector(), Detector.UastScanner { - override fun getApplicableUastTypes(): List> { - return listOf( - UCallExpression::class.java, - UQualifiedReferenceExpression::class.java, - ) - } + override fun getApplicableUastTypes(): List> = listOf( + UCallExpression::class.java, + UQualifiedReferenceExpression::class.java, + ) - override fun createUastHandler(context: JavaContext): UElementHandler { - return object : UElementHandler() { + override fun createUastHandler(context: JavaContext): UElementHandler = + object : UElementHandler() { override fun visitCallExpression(node: UCallExpression) { val name = node.methodName ?: return val preferredName = METHOD_NAMES[name] ?: return @@ -55,7 +53,6 @@ class DesignSystemDetector : Detector(), Detector.UastScanner { reportIssue(context, node, name, preferredName) } } - } companion object { @JvmField