From f018087293e7f64d36497f070ea40e2ce4af0ed1 Mon Sep 17 00:00:00 2001 From: TM Date: Sun, 22 Dec 2024 02:42:24 -0800 Subject: [PATCH 1/8] ErrorMonitor Interface/Implementation with MessageData Model --- .../core/data/util/ErrorMonitor.kt | 35 ++++++++++ .../core/data/util/StateErrorMonitor.kt | 67 +++++++++++++++++++ .../core/model/data/MessageData.kt | 37 ++++++++++ 3 files changed, 139 insertions(+) create mode 100644 core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ErrorMonitor.kt create mode 100644 core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/StateErrorMonitor.kt create mode 100644 core/model/src/main/kotlin/com/google/samples/apps/nowinandroid/core/model/data/MessageData.kt diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ErrorMonitor.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ErrorMonitor.kt new file mode 100644 index 0000000000..e5d967a864 --- /dev/null +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ErrorMonitor.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.data.util + +import com.google.samples.apps.nowinandroid.core.model.data.MessageData +import kotlinx.coroutines.flow.Flow + +/** + * Interface for handling messages. + */ +interface ErrorMonitor { + fun addMessageByString(message: String): MessageData + + fun addMessageByData(message: MessageData) + + fun clearMessage(message: MessageData) + + fun clearAllMessages() + + val messages: Flow> +} diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/StateErrorMonitor.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/StateErrorMonitor.kt new file mode 100644 index 0000000000..76efe3e379 --- /dev/null +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/StateErrorMonitor.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.data.util + +import com.google.samples.apps.nowinandroid.core.model.data.MessageData +import com.google.samples.apps.nowinandroid.core.model.data.MessageType +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +/** + * Interface implementation for handling general errors. + */ + +class StateErrorMonitor @Inject constructor() : ErrorMonitor { + /** + * List of [MessageData] to be shown + */ + override val messages = MutableStateFlow>(emptyList()) + + /** + * Creates a [MessageData] and adds it to the list. + * @param message: String value for message to add. + */ + override fun addMessageByString(message: String): MessageData { + val data = MessageData(type = MessageType.MESSAGE(message)) + messages.update { it + data } + + return data + } + + /** + * Add a [MessageData] to the list. + * @param message: [MessageData] to add. + */ + override fun addMessageByData(message: MessageData) { + messages.update { it + message } + } + + /** + * Removes the [MessageData] from the list. + */ + override fun clearMessage(message: MessageData) { + messages.update { list -> list.filterNot { it == message } } + } + + /** + * Remove all from list, reset to empty list + */ + override fun clearAllMessages() { + messages.update { emptyList() } + } +} diff --git a/core/model/src/main/kotlin/com/google/samples/apps/nowinandroid/core/model/data/MessageData.kt b/core/model/src/main/kotlin/com/google/samples/apps/nowinandroid/core/model/data/MessageData.kt new file mode 100644 index 0000000000..7fc5eb8f29 --- /dev/null +++ b/core/model/src/main/kotlin/com/google/samples/apps/nowinandroid/core/model/data/MessageData.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.model.data + +/** + * [MessageData] + * Class to hold messages type objects with actions + */ +data class MessageData( + val type: MessageType, + val label: String? = null, + val onConfirm: (() -> Unit)? = null, + val onDelay: (() -> Unit)? = null, +) + +/** + * Specified Errors + */ +sealed class MessageType { + data object OFFLINE : MessageType() + data class MESSAGE(val value: String) : MessageType() + data object UNKNOWN : MessageType() +} From a7952aaf9da6c53be1e2060fbd0a942721f9f7cb Mon Sep 17 00:00:00 2001 From: TM Date: Sun, 22 Dec 2024 02:42:29 -0800 Subject: [PATCH 2/8] Create StateErrorMonitorTest.kt --- .../core/data/util/StateErrorMonitorTest.kt | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/util/StateErrorMonitorTest.kt diff --git a/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/util/StateErrorMonitorTest.kt b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/util/StateErrorMonitorTest.kt new file mode 100644 index 0000000000..3e928cea76 --- /dev/null +++ b/core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/util/StateErrorMonitorTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.samples.apps.nowinandroid.core.data.util.StateErrorMonitor +import com.google.samples.apps.nowinandroid.core.model.data.MessageData +import com.google.samples.apps.nowinandroid.core.model.data.MessageType +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +/** + * Unit tests for [StateErrorMonitor]. + */ + +class StateErrorMonitorTest { + + // Subject under test. + private lateinit var state: StateErrorMonitor + + private lateinit var messages: List + + private val testString = "Test Error Message" + private val testData = MessageData(MessageType.MESSAGE("Test Error Message 1")) + private val testData2 = MessageData(MessageType.MESSAGE("Test Error Message 2")) + + @Before + fun setup() { + state = StateErrorMonitor() + messages = emptyList() + } + + @Test + fun whenErrorIsNotAdded_NullIsPresent() = runTest(UnconfinedTestDispatcher()) { + backgroundScope.launch { + state.messages.collect { + messages = it + } + } + assertEquals( + emptyList(), + messages, + ) + } + + @Test + fun whenErrorIsAddedByString_ErrorMessageIsPresent() = runTest(UnconfinedTestDispatcher()) { + backgroundScope.launch { + state.messages.collect { + messages = it + } + } + + val expect = state.addMessageByString(testString) + + assertEquals( + expect, + messages.firstOrNull(), + ) + } + + @Test + fun whenErrorIsAddedByData_ErrorMessageIsPresent() = runTest(UnconfinedTestDispatcher()) { + backgroundScope.launch { + state.messages.collect { + messages = it + } + } + + state.addMessageByData(testData) + + assertEquals( + testData, + messages.firstOrNull(), + ) + } + + @Test + fun whenErrorsAreAdded_FirstErrorMessageIsPresent() = + runTest(UnconfinedTestDispatcher()) { + state.addMessageByData(testData) + state.addMessageByString(testString) + + backgroundScope.launch { + state.messages.collect { + messages = it + } + } + + assertEquals( + testData, + messages.firstOrNull(), + ) + } + + @Test + fun whenErrorIsCleared_ErrorMessageIsNotPresent() = + runTest(UnconfinedTestDispatcher()) { + backgroundScope.launch { + state.messages.collect { + messages = it + } + } + state.addMessageByData(testData) + state.clearMessage(testData) + + assertEquals( + emptyList(), + messages, + ) + } + + @Test + fun whenErrorsAreCleared_NextErrorMessageIsPresent() = + runTest(UnconfinedTestDispatcher()) { + backgroundScope.launch { + state.messages.collect { + messages = it + } + } + state.addMessageByData(testData) + state.addMessageByData(testData2) + + state.clearMessage(testData) + + assertEquals( + testData2, + messages.firstOrNull(), + ) + } +} From eb75ebb33021a77e81d9cb680dbfdb10dd503ab0 Mon Sep 17 00:00:00 2001 From: TM Date: Sun, 22 Dec 2024 02:47:25 -0800 Subject: [PATCH 3/8] Add Messages to AppState --- .../apps/nowinandroid/ui/NiaAppState.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) 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 7c892c854e..39ab800c59 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 @@ -31,8 +31,11 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import androidx.tracing.trace import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository +import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor +import com.google.samples.apps.nowinandroid.core.model.data.MessageData +import com.google.samples.apps.nowinandroid.core.model.data.MessageType import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou @@ -53,6 +56,7 @@ import kotlinx.datetime.TimeZone @Composable fun rememberNiaAppState( networkMonitor: NetworkMonitor, + errorMonitor: ErrorMonitor, userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, coroutineScope: CoroutineScope = rememberCoroutineScope(), @@ -63,6 +67,7 @@ fun rememberNiaAppState( navController, coroutineScope, networkMonitor, + errorMonitor, userNewsResourceRepository, timeZoneMonitor, ) { @@ -70,6 +75,7 @@ fun rememberNiaAppState( navController = navController, coroutineScope = coroutineScope, networkMonitor = networkMonitor, + errorMonitor = errorMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) @@ -81,6 +87,7 @@ class NiaAppState( val navController: NavHostController, coroutineScope: CoroutineScope, networkMonitor: NetworkMonitor, + val errorMonitor: ErrorMonitor, userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, ) { @@ -115,6 +122,27 @@ class NiaAppState( initialValue = false, ) + private val errorMessages: StateFlow> = errorMonitor.messages.stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) + + val stateMessage: StateFlow = combine(isOffline, errorMessages) { offline, errors -> + if (offline) { + // Priority is given to Offline Error Message over other types + MessageData(type = MessageType.OFFLINE) + } + // Otherwise, Display first from error monitor list + else { + errors.first() + } + }.stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + /** * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the * route. From cb348882513def4294d78e2a7cf84ee6bc9e2251 Mon Sep 17 00:00:00 2001 From: TM Date: Sun, 22 Dec 2024 02:56:06 -0800 Subject: [PATCH 4/8] StateMessages displayed in Snackbar Including offline mesage in snackbar-bound state messages --- .../samples/apps/nowinandroid/ui/NiaApp.kt | 49 +++++++++++++++---- app/src/main/res/values/strings.xml | 1 + 2 files changed, 41 insertions(+), 9 deletions(-) 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 f27b90cbea..e23b573334 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 @@ -16,6 +16,7 @@ package com.google.samples.apps.nowinandroid.ui +import android.content.Context import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets @@ -30,6 +31,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarDuration.Short import androidx.compose.material3.SnackbarHost @@ -52,6 +54,7 @@ import androidx.compose.ui.composed import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics @@ -69,6 +72,10 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAp import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors +import com.google.samples.apps.nowinandroid.core.model.data.MessageData +import com.google.samples.apps.nowinandroid.core.model.data.MessageType.MESSAGE +import com.google.samples.apps.nowinandroid.core.model.data.MessageType.OFFLINE +import com.google.samples.apps.nowinandroid.core.model.data.MessageType.UNKNOWN import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination @@ -81,6 +88,8 @@ fun NiaApp( modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { + val context = LocalContext.current + val shouldShowGradientBackground = appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU var showSettingsDialog by rememberSaveable { mutableStateOf(false) } @@ -95,16 +104,30 @@ fun NiaApp( ) { val snackbarHostState = remember { SnackbarHostState() } - val isOffline by appState.isOffline.collectAsStateWithLifecycle() + val stateMessage by appState.stateMessage.collectAsStateWithLifecycle() - // If user is not connected to the internet show a snack bar to inform them. - val notConnectedMessage = stringResource(R.string.not_connected) - LaunchedEffect(isOffline) { - if (isOffline) { - snackbarHostState.showSnackbar( - message = notConnectedMessage, - duration = Indefinite, - ) + LaunchedEffect(stateMessage) { + stateMessage?.let { message -> + + //Text and Duration values dictated by the UI + val (text, duration) = getSnackbarValues(context, message) + + // Determine whether user clicked action button + val snackBarResult = snackbarHostState.showSnackbar( + message = text, + actionLabel = message.label, + duration = duration, + ) == ActionPerformed + + // Handle result action + if (snackBarResult) { + message.onConfirm?.invoke() + } else { + message.onDelay?.invoke() + } + + // Remove Message from List + appState.errorMonitor.clearMessage(message) } } @@ -271,3 +294,11 @@ private fun NavDestination?.isRouteInHierarchy(route: KClass<*>) = this?.hierarchy?.any { it.hasRoute(route) } ?: false + +private fun getSnackbarValues(context: Context, message: MessageData): Pair { + return when (message.type) { + OFFLINE -> context.getString(R.string.not_connected) to SnackbarDuration.Indefinite + is MESSAGE -> (message.type as MESSAGE).value to SnackbarDuration.Long + UNKNOWN -> context.getString(R.string.unknown_error) to SnackbarDuration.Short + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd92f3977b..2f8cbe60e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,4 +17,5 @@ Now in Android ⚠️ You aren’t connected to the internet + Unknown Error From 371685a6c6da72fc726d16bacb5ebfe7f527c12b Mon Sep 17 00:00:00 2001 From: TM Date: Sun, 22 Dec 2024 03:05:13 -0800 Subject: [PATCH 5/8] Injection for ErrorMonitor --- .../com/google/samples/apps/nowinandroid/MainActivity.kt | 5 +++++ .../samples/apps/nowinandroid/core/data/di/DataModule.kt | 7 +++++++ 2 files changed, 12 insertions(+) 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 ecc23d80e0..17e1db4686 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 @@ -37,6 +37,7 @@ import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository +import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -64,6 +65,9 @@ class MainActivity : ComponentActivity() { @Inject lateinit var networkMonitor: NetworkMonitor + @Inject + lateinit var errorMonitor: ErrorMonitor + @Inject lateinit var timeZoneMonitor: TimeZoneMonitor @@ -135,6 +139,7 @@ class MainActivity : ComponentActivity() { setContent { val appState = rememberNiaAppState( networkMonitor = networkMonitor, + errorMonitor = errorMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/di/DataModule.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/di/DataModule.kt index fa4bde8b83..ceb65b3e27 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/di/DataModule.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/di/DataModule.kt @@ -27,7 +27,9 @@ import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsR import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.util.ConnectivityManagerNetworkMonitor +import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor +import com.google.samples.apps.nowinandroid.core.data.util.StateErrorMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneBroadcastMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor import dagger.Binds @@ -69,6 +71,11 @@ abstract class DataModule { networkMonitor: ConnectivityManagerNetworkMonitor, ): NetworkMonitor + @Binds + internal abstract fun bindsErrorMonitor( + errorMonitor: StateErrorMonitor, + ): ErrorMonitor + @Binds internal abstract fun binds(impl: TimeZoneBroadcastMonitor): TimeZoneMonitor } From ff7ffb1c8e1c083e969a091a81e17792b22ef210 Mon Sep 17 00:00:00 2001 From: TM Date: Thu, 2 Jan 2025 18:25:32 -0800 Subject: [PATCH 6/8] Added Test and Fake ErrorMonitors, updated tests Fake Error Monitor as EmptyErrorMonitor, containing no messages --- .../samples/apps/nowinandroid/ui/NiaApp.kt | 4 +- .../ui/NiaAppScreenSizesScreenshotTests.kt | 5 ++ .../apps/nowinandroid/ui/NiaAppStateTest.kt | 9 +++ .../ui/SnackbarInsetsScreenshotTests.kt | 5 ++ .../ui/SnackbarScreenshotTests.kt | 5 ++ .../core/data/test/EmptyErrorMonitor.kt | 46 ++++++++++++ .../core/data/test/TestDataModule.kt | 6 ++ .../core/testing/util/TestErrorMonitor.kt | 73 +++++++++++++++++++ 8 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/EmptyErrorMonitor.kt create mode 100644 core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestErrorMonitor.kt 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 e23b573334..41c0eb4074 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 @@ -109,7 +109,7 @@ fun NiaApp( LaunchedEffect(stateMessage) { stateMessage?.let { message -> - //Text and Duration values dictated by the UI + // Text and Duration values dictated by the UI val (text, duration) = getSnackbarValues(context, message) // Determine whether user clicked action button @@ -301,4 +301,4 @@ private fun getSnackbarValues(context: Context, message: MessageData): Pair (message.type as MESSAGE).value to SnackbarDuration.Long UNKNOWN -> context.getString(R.string.unknown_error) to SnackbarDuration.Short } -} \ 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 9c9488fde4..47bd1baf3a 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 @@ -32,6 +32,7 @@ import com.github.takahirom.roborazzi.captureRoboImage import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository +import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -80,6 +81,9 @@ class NiaAppScreenSizesScreenshotTests { @Inject lateinit var networkMonitor: NetworkMonitor + @Inject + lateinit var errorMonitor: ErrorMonitor + @Inject lateinit var timeZoneMonitor: TimeZoneMonitor @@ -123,6 +127,7 @@ class NiaAppScreenSizesScreenshotTests { NiaTheme { val fakeAppState = rememberNiaAppState( networkMonitor = networkMonitor, + errorMonitor = errorMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index c6ddb54fbe..1dba37a6b1 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -27,8 +27,10 @@ import androidx.navigation.compose.composable import androidx.navigation.createGraph import androidx.navigation.testing.TestNavHostController import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository +import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository +import com.google.samples.apps.nowinandroid.core.testing.util.TestErrorMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor import dagger.hilt.android.testing.HiltAndroidTest @@ -43,6 +45,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import javax.inject.Inject import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -60,6 +63,8 @@ class NiaAppStateTest { // Create the test dependencies. private val networkMonitor = TestNetworkMonitor() + private var errorMonitor = TestErrorMonitor() + private val timeZoneMonitor = TestTimeZoneMonitor() private val userNewsResourceRepository = @@ -79,6 +84,7 @@ class NiaAppStateTest { navController = navController, coroutineScope = backgroundScope, networkMonitor = networkMonitor, + errorMonitor = errorMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) @@ -101,6 +107,7 @@ class NiaAppStateTest { composeTestRule.setContent { state = rememberNiaAppState( networkMonitor = networkMonitor, + errorMonitor = errorMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) @@ -119,6 +126,7 @@ class NiaAppStateTest { navController = NavHostController(LocalContext.current), coroutineScope = backgroundScope, networkMonitor = networkMonitor, + errorMonitor = errorMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) @@ -139,6 +147,7 @@ class NiaAppStateTest { navController = NavHostController(LocalContext.current), coroutineScope = backgroundScope, networkMonitor = networkMonitor, + errorMonitor = errorMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt index 78f568e039..e464eaffea 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt @@ -63,6 +63,7 @@ import com.github.takahirom.roborazzi.captureRoboImage import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository +import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -113,6 +114,9 @@ class SnackbarInsetsScreenshotTests { @Inject lateinit var networkMonitor: NetworkMonitor + @Inject + lateinit var errorMonitor: ErrorMonitor + @Inject lateinit var timeZoneMonitor: TimeZoneMonitor @@ -254,6 +258,7 @@ class SnackbarInsetsScreenshotTests { NiaTheme { val appState = rememberNiaAppState( networkMonitor = networkMonitor, + errorMonitor = errorMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt index b9b1047c10..284ccc90d1 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt @@ -36,6 +36,7 @@ import com.github.takahirom.roborazzi.captureRoboImage import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository +import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme @@ -86,6 +87,9 @@ class SnackbarScreenshotTests { @Inject lateinit var networkMonitor: NetworkMonitor + @Inject + lateinit var errorMonitor: ErrorMonitor + @Inject lateinit var timeZoneMonitor: TimeZoneMonitor @@ -203,6 +207,7 @@ class SnackbarScreenshotTests { NiaTheme { val appState = rememberNiaAppState( networkMonitor = networkMonitor, + errorMonitor = errorMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) diff --git a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/EmptyErrorMonitor.kt b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/EmptyErrorMonitor.kt new file mode 100644 index 0000000000..f12ba06905 --- /dev/null +++ b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/EmptyErrorMonitor.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2025 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.core.data.test + +import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor +import com.google.samples.apps.nowinandroid.core.model.data.MessageData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +class EmptyErrorMonitor @Inject constructor(): ErrorMonitor { + + override fun addMessageByString(message: String): MessageData { + TODO("Not yet implemented") + } + + override fun addMessageByData(message: MessageData) { + TODO("Not yet implemented") + } + + override fun clearMessage(message: MessageData) { + TODO("Not yet implemented") + } + + override fun clearAllMessages() { + TODO("Not yet implemented") + } + + override val messages: Flow> + get() = flowOf(emptyList()) + +} \ No newline at end of file diff --git a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/TestDataModule.kt b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/TestDataModule.kt index 46158479c9..4d48123bcf 100644 --- a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/TestDataModule.kt +++ b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/TestDataModule.kt @@ -27,6 +27,7 @@ import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeRecent import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeSearchContentsRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeTopicsRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository +import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor import dagger.Binds @@ -70,6 +71,11 @@ internal interface TestDataModule { networkMonitor: AlwaysOnlineNetworkMonitor, ): NetworkMonitor + @Binds + fun bindsErrorMonitor( + errorMonitor: EmptyErrorMonitor, + ): ErrorMonitor + @Binds fun binds(impl: DefaultZoneIdTimeZoneMonitor): TimeZoneMonitor } diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestErrorMonitor.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestErrorMonitor.kt new file mode 100644 index 0000000000..64840337a5 --- /dev/null +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestErrorMonitor.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2025 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.core.testing.util + +import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor +import com.google.samples.apps.nowinandroid.core.model.data.MessageData +import com.google.samples.apps.nowinandroid.core.model.data.MessageType +import com.google.samples.apps.nowinandroid.core.model.data.MessageType.MESSAGE +import com.google.samples.apps.nowinandroid.core.model.data.MessageType.OFFLINE +import com.google.samples.apps.nowinandroid.core.model.data.MessageType.UNKNOWN +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf + +class TestErrorMonitor : ErrorMonitor { + + private val _messages = MutableStateFlow>(emptyList()) + + override fun addMessageByString(message: String): MessageData { + TODO("Not yet implemented") + } + + override fun addMessageByData(message: MessageData) { + TODO("Not yet implemented") + } + + override fun clearMessage(message: MessageData) { + TODO("Not yet implemented") + } + + override fun clearAllMessages() { + _messages.value = emptyList() + } + + override val messages: Flow> + get() = _messages + + /** + * Test-only API to add message types + */ + + fun setOfflineMessage(){ + _messages.value = listOf(OFFLINE_MESSAGE) + } + + fun addMessage(){ + _messages.value.plus(MESSAGE_MESSAGE) + } + + fun addUnknownMessage(){ + _messages.value.plus(UNKNOWN_MESSAGE) + } + + companion object{ + val OFFLINE_MESSAGE = MessageData(OFFLINE) + val MESSAGE_MESSAGE = MessageData(MESSAGE("Message"), "Title", {}, {}) + val UNKNOWN_MESSAGE = MessageData(UNKNOWN) + } +} \ No newline at end of file From 9e02a3b3faba1cf73340899d188b32f2eeb66876 Mon Sep 17 00:00:00 2001 From: TM Date: Thu, 2 Jan 2025 18:43:25 -0800 Subject: [PATCH 7/8] Spotless Apply --- .../samples/apps/nowinandroid/ui/NiaAppStateTest.kt | 2 -- .../nowinandroid/core/data/test/EmptyErrorMonitor.kt | 5 ++--- .../core/testing/util/TestErrorMonitor.kt | 12 +++++------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt index 1dba37a6b1..97132aa721 100644 --- a/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt +++ b/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt @@ -27,7 +27,6 @@ import androidx.navigation.compose.composable import androidx.navigation.createGraph import androidx.navigation.testing.TestNavHostController import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository -import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.util.TestErrorMonitor @@ -45,7 +44,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -import javax.inject.Inject import kotlin.test.assertEquals import kotlin.test.assertTrue diff --git a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/EmptyErrorMonitor.kt b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/EmptyErrorMonitor.kt index f12ba06905..5eee73d69f 100644 --- a/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/EmptyErrorMonitor.kt +++ b/core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/EmptyErrorMonitor.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import javax.inject.Inject -class EmptyErrorMonitor @Inject constructor(): ErrorMonitor { +class EmptyErrorMonitor @Inject constructor() : ErrorMonitor { override fun addMessageByString(message: String): MessageData { TODO("Not yet implemented") @@ -42,5 +42,4 @@ class EmptyErrorMonitor @Inject constructor(): ErrorMonitor { override val messages: Flow> get() = flowOf(emptyList()) - -} \ No newline at end of file +} diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestErrorMonitor.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestErrorMonitor.kt index 64840337a5..ca28650a50 100644 --- a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestErrorMonitor.kt +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/util/TestErrorMonitor.kt @@ -18,13 +18,11 @@ package com.google.samples.apps.nowinandroid.core.testing.util import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor import com.google.samples.apps.nowinandroid.core.model.data.MessageData -import com.google.samples.apps.nowinandroid.core.model.data.MessageType import com.google.samples.apps.nowinandroid.core.model.data.MessageType.MESSAGE import com.google.samples.apps.nowinandroid.core.model.data.MessageType.OFFLINE import com.google.samples.apps.nowinandroid.core.model.data.MessageType.UNKNOWN import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flowOf class TestErrorMonitor : ErrorMonitor { @@ -53,21 +51,21 @@ class TestErrorMonitor : ErrorMonitor { * Test-only API to add message types */ - fun setOfflineMessage(){ + fun setOfflineMessage() { _messages.value = listOf(OFFLINE_MESSAGE) } - fun addMessage(){ + fun addMessage() { _messages.value.plus(MESSAGE_MESSAGE) } - fun addUnknownMessage(){ + fun addUnknownMessage() { _messages.value.plus(UNKNOWN_MESSAGE) } - companion object{ + companion object { val OFFLINE_MESSAGE = MessageData(OFFLINE) val MESSAGE_MESSAGE = MessageData(MESSAGE("Message"), "Title", {}, {}) val UNKNOWN_MESSAGE = MessageData(UNKNOWN) } -} \ No newline at end of file +} From b566b07b3506ee54406c22a5adfcada8fe4cfe31 Mon Sep 17 00:00:00 2001 From: TM Date: Fri, 3 Jan 2025 01:33:45 -0800 Subject: [PATCH 8/8] Update appstate error message null check --- .../com/google/samples/apps/nowinandroid/ui/NiaAppState.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 39ab800c59..215431ff28 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 @@ -133,9 +133,9 @@ class NiaAppState( // Priority is given to Offline Error Message over other types MessageData(type = MessageType.OFFLINE) } - // Otherwise, Display first from error monitor list + // Otherwise, Display first from error monitor list if exists else { - errors.first() + errors.firstOrNull() } }.stateIn( scope = coroutineScope,